diff --git a/designs/default-zero-value.md b/designs/default-zero-value.md deleted file mode 100644 index 404b2082214..00000000000 --- a/designs/default-zero-value.md +++ /dev/null @@ -1,778 +0,0 @@ -# Default zero values - -* **Author**: Michael Dowling -* **Created**: 2021-08-24 -* **Last updated**: 2022-06-08 - - -## Abstract - -This document describes a fundamental change to the way Smithy describes -structure member nullability (also known as optionality). By adding a -`@default` trait to structure members, code generators can generate non-null -accessors (or getters) for members marked as `@default` or `@required` -(with some caveats for backward compatibility). While this proposal is not -AWS-specific, the practical impact of it is that when implemented, it will -convert thousands of optional property accessors to non-optional in the -Rust, Kotlin, and Swift AWS SDKs. - -This proposal requires breaking changes to Smithy and proposes that the model -move to version 2.0. - - -## Terms used in this proposal - -* null: This document uses the term null, nullable, and nullability to refer - to members that optionally have a value. Some programming languages don't - have a concept of `null` and use other forms of presence tracking like - `Option` in Rust or `Maybe` in Haskell. -* accessor, getter: In this proposal, the terms "accessor" and "getter" should - be considered generic terms that translate to however a programming language - exposes structure member values. For example, some programming languages like - Java often use getter methods while others might expose struct style - properties directly. - - -## Motivation - -Most structure member accessors generated from Smithy 1.0 models return -nullable values. As new languages like Rust and Kotlin that explicitly model -nullability in their type systems adopt Smithy, excessive nullability in -generated code becomes burdensome to end-users because they need to call -methods like `.unwrap()` on everything. Generating every structure member as -nullable makes it hard for customers to know when it is safe to dereference a -value and when it will result in an error. Adding the ability to control when -a value is nullable vs when a value is always present provides an ergonomic and -safety benefit to end users of code generated types. - -For example, after this proposal is implemented, the following Rust code: - -```rust -println!("Lexicons:"); -let lexicons = resp.lexicons.unwrap_or_default(); -for lexicon in &lexicons { - println!( - " Name: {}", - lexicon.name.as_deref().unwrap_or_default() - ); - println!( - " Language: {:?}\n", - lexicon - .attributes - .as_ref() - .map(|attrib| attrib - .language_code - .as_ref() - .expect("languages must have language codes")) - .expect("languages must have attributes") - ); -} -println!("\nFound {} lexicons.\n", lexicons.len()); -``` - -Can be simplified to: - -```rust -println!("Lexicons:"); -for lexicon in &resp.lexicons { - println!(" Name: {}", lexicon.name); - println!(" Languages: {:?}\n", - lexicon.attributes.map(|attrib| attrib.language_code) - ); -} -println!("\nFound {} lexicons.\n", resp.lexicons.len()); -``` - - -## Background - -Nullability in Smithy IDL 1.0 is controlled in the following ways: - -1. Map keys, values in sets, and unions cannot contain null values. This - proposal does not change this behavior. -2. List and map values cannot contain null values by default. The `@sparse` - trait applied to list and map shapes allow them to contain null entries. - These kinds of collections rarely need to contain nullable members. This - proposal does not change this behavior. -3. The nullability of structure members in Smithy today is resolved using the - following logic: - 1. If the member is marked with `@box`, it's nullable. - 2. If the shape targeted by the member is marked with `@box`, it's nullable. - 3. If the shape targeted by the member is a byte, short, integer, long, - float, double, or boolean, it's non-null. - 4. All other members are considered nullable. - - -### Why was the `@required` trait unreliable for codegen? - -Removing the `@required` trait from a structure member has historically been -considered backwards compatible in Smithy because it is loosening a restriction. -The `@required` trait in Smithy 1.0 is categorized as a constraint trait and -only used for validation rather than to influence generated types. This gives -service teams the flexibility to remove the required trait as needed without -breaking generated client code. The `@required` trait might be removed if new -use cases emerge where a member is only conditionally required, and more -rarely, it might be added if the service team accidentally omitted the trait -when the service initially launched. - -For context, as of May 2021, the `@required` trait has been removed from a -structure member in 105 different AWS services across 618 different members. -Encoding the `@required` trait into generated types would have made changing a -member from required to optional a breaking change to generated code, which is -something most services try to avoid given its frequency and the cost of -shipping a major version of a service and client. While this data is -AWS-specific, it's indicative of how often disparate teams that use Smithy -(or its predecessor in of Amazon) sought to update their APIs so that members -can become optional. - - -## Goals and non-goals - -**Goals** - -1. Reduce the amount of nullability in code generated from Smithy models. -2. Allow for similar backward compatibility guarantees that exist today like - being able to backward compatibly remove the `@required` trait from a - structure member without breaking previously generated clients. -3. Maintain Smithy's protocol-agnostic design. Protocols should never influence - how types are generated from Smithy models; they're only intended to change - how types are serialized and deserialized. In practice, this means that - features of a protocol that extend beyond Smithy's metamodel and definitions - of structure member nullability should not be exposed in code generated - types that represent Smithy shapes. -4. We should be able to use the same code-generated types for clients, servers, - and standalone type codegen. - -**Non-goals** - -1. Mandate how protocols are designed. This proposal intends to define - requirements and affordances about how nullability is defined the Smithy - metamodel, and in turn, in code generated from Smithy models. - - -## High-level summary - -This proposal introduces a `@default` trait, introduces a `@clientOptional` -trait, removes the `@box` trait, removes the `Primitive*` shapes from the -prelude, and makes the nullability of a structure member something that is -completely controlled by members rather than the shape targeted by a member. - -The `@default` trait initializes a structure member with a default, zero value. - -``` -structure Message { - @required - title: String - - @default - message: String -} -``` - -In the above example: - -- `title` is marked as `@required`, so it is non-null in generated code. -- `message` is also non-null in generated code and is assigned a default, zero - value of `""` when not explicitly provided. This makes it easier to use - `title` and `message` in code without having to first check if the value is - `null`. For example, you can call `message.message.size()` without first - checking if the value is non-null. - -If the `title` member ever needs to be made optional, the `@required` trait -can be replaced with the `@default` trait: - -``` -structure Message { - @default - title: String - - @default - message: String -} -``` - -With the above change, codegen remains the same: both values are non-null. -However, if `title` is not set in client code, server-side validation for the -type will _not_ fail because `title` has a default, zero value of `""`. The -trade-off to this change is that it is impossible to tell if `title` was -omitted or explicitly provided. - - -## Proposal overview - -We will introduce a 2.0 of Smithy models that simplifies nullability by moving -nullability controls from shapes to structure members. Smithy IDL 2.0 will: - -1. Add a `@default` trait that can target structure members. Structure - members only have a default zero value if they are marked with this trait. - Default zero values are no longer controlled based on the shape targeted by - a member, localizing this concern to members. This makes nullability of a - member easier to understand for both readers and writers. -2. Add a `@clientOptional` trait that can target structure members. This trait - is essentially a more constrained and better named `@box` trait. The primary - use case for this trait is to apply it to members also marked as - `@required` to indicate to non-authoritative code generators like clients - that a service reserves the right to remove the `@required` trait from a - member without a major version bump of the service. -3. Remove the `@box` trait from the Smithy 2.0 prelude and fail to load models - that contain the `@box` trait. -4. Remove the `PrimitiveBoolean`, `PrimitiveShort`, `PrimitiveInteger`, - `PrimitiveLong`, `PrimitiveFloat`, and `PrimitiveDouble` shapes from the - Smithy 2.0 prelude. IDL 2.0 models will fail if they use these shapes. -5. Update the Smithy IDL 2.0 model loader implementation to be able to load - Smithy 1.0 models alongside Smithy 2.0 models. - 1. Inject the `@default` trait on structure members that targeted shapes - with a default zero value and were not marked with the `@box` trait. - 2. Replace the `@box` trait with `@clientOptional` on structure members. - 3. Remove the `@box` trait from non-members. - 4. Rewrite members that target one of the removed Primitive* shapes to - target the corresponding non-primitive shape in the prelude (for example, - `PrimitiveInteger` is rewritten to target `Integer`). - - -## `@default` trait - -The `@default` trait can be applied to structure members to indicate that the -targeted shape has a default, zero value. The `@default` trait *does not* allow -for a custom default value. - -The following example defines a structure with a `@default` "title" member that -has a default zero value: - -``` -structure Message { - @default - title: String // defaults to "" -} -``` - -The `@default` trait is defined in Smithy as: - -``` -/// Provides a structure member with a default zero value. -@trait( - selector: "structure > member :test(> :is(simpleType, collection, map))", - conflicts: [required], - // The default trait can never be removed. It can only be added if the - // member was previously marked as required. - breakingChanges: [{change: "remove"}] -) -structure default {} -``` - - -### **Default zero values** - -The following list describes the default zero value of each kind of shape. -Programming languages and code generators that cannot initialize structure -members with the following default values SHOULD continue to represent those -members as nullable as this is semantically equivalent to the default zero -value. - -- Boolean: boolean false (`false`) -- Numbers: numeric zero (`0`) -- String: an empty string (`""`). Strings with the enum trait also have a - default value of "". -- Blob: an empty blob. This includes blob shapes marked as `@streaming`. -- Timestamp: zero seconds since the epoch (for example, `0`, or - `1970-01-01T00:00:00Z`). -- Document: a null document value. -- List: an empty list (`[]`). -- Set: an empty set (`[]`). -- Map: an empty map (`{}`). -- Structure: Structures have no default zero value. -- Union: no default zero value. A union MUST be set to one of its variants for - it to be valid, and unions have no default variant. - - -### Impact on API design - -Members marked with the `@default` trait cannot differentiate between whether a -property was set to its zero value or if a property was omitted. Default zero -values are part of the type system of Smithy and not specific to any particular -serialization of a shape. This makes the `@default` trait a poor choice for -partial updates or patch style APIs where it is critical to differentiate -between omitted values and explicitly set values. - -To help guide API design, built-in validation will be added to Smithy to detect -and warn when `@default` members are detected in the top-level input of -operations that start with `Update`, operation bound to the `update` lifecycle -of a resource, or operations that use the `PATCH` HTTP method. - -For example, the following model: - -``` -$version: "2" -namespace smithy.examnple - -operation UpdateUser { - input: UpdateUserInput -} - -structure UpdateUserInput { - @default - username: String -} -``` - -Would emit a warning similar to the following: - -``` -WARNING: smithy.example#UpdateUserInput$userName (DefaultOnUpdate) - @ /path/to/main.smithy - | - 4 | operation UpdateUser { - | ^ - = This update style operation has top-level input members marked with the - @default trait. It will be impossible to tell if the member was omitted - or explicitly provided. Affected members: [UpdateUserInput$username]. -``` - - -### Constraint trait validation - -Constraint traits are not evaluated on structure members marked with the -`@default` trait when the value of the member is the default value. - -Consider the following structure: - -``` -structure Foo { - @default - @range(min: 5, max: 10) - number: Integer -} -``` - -The ``Foo$number`` member is marked with the `@range` trait, requiring a value -between 5 and 10 inclusive. Because it is marked with the `@default` trait, -the default value, `0`, is also permitted. - -A member marked with the `@default` trait implicitly adds the default value -of the member to its set of permitted values. This is equivalent to how -optional members that are not marked with `@default` or `@required` implicitly -allow for null or omitted values. - - -## `@clientOptional` trait - -What is required today might not be required tomorrow. For cases when a service -isn't sure if a member will be required forever, they can mark a `@required` -member as `@clientOptional` to ensure that non-authoritative consumers of the -model like clients treat the member as optional. The `@required` trait can be -backward compatibly removed from a member marked as `@clientOptional` (and not -replaced with the `@default` trait). This causes the `@required` trait to -function as server-side validation rather than something that changes generated -code. - -Structure members in Smithy are automatically considered nullable. For example, -the following structure: - -``` -structure Foo { - baz: String -} -``` - -Is equivalent to the following structure: - -``` -structure Foo { - @clientOptional - baz: String -} -``` - -The primary use case of the `@clientOptional` trait is to indicate that while a -member is _currently_ defined as `@required`, the service reserves the right to -later remove the `@required` trait and make the member optional in the future. - -For example, the `@required` trait on `foo` in the following structure is -considered a validation constraint rather than a type refinement trait: - -``` -structure Foo { - @required - @clientOptional - foo: String -} -``` - -The `@clientOptional` trait is defined in Smithy as: - -``` -@trait(selector: "structure > member") -structure clientOptional {} -``` - -The `@clientOptional` trait also causes members marked with the `@default` -trait to be considered optional: - -``` -structure Message { - @default - @clientOptional - title: String -} -``` - -### @input structures apply @clientOptional - -Required members of a structure marked with the `@input` trait are implicitly -considered to be marked with `@clientOptional`. The `@input` trait special-cases -a structure as the input of a single operation that cannot be referenced in any -other place in the model. Structures marked with the `@input` have more relaxed -backward compatibility guarantees. It is backward compatible to remove the -`@required` trait from top-level members of structures marked with the `@input` -trait, and the `@required` trait does not need to be replaced with the -`@default` trait (though this is allowed as well). This gives service teams the -ability to remove the `@required` trait from top-level input members and loosen -requirements without risking breaking previously generated clients. - -The practical implication of this backward compatibility affordance is that -code generated types for members of an `@input` structure MUST all be -considered nullable regardless of the use of `@required` or `@default`. Not -observing these nullability affordances runs the risk of previously generated -code breaking when a model is updated in the future. - -Organizations that want stricter nullability controls over inputs can choose to -not use the `@input` trait. - - -## Backward compatibility rules - -Backward compatibility rules of the `@default`, `@required`, and -`@clientOptional` traits are as follows: - -- The `@default` trait can never be removed from a member. -- The `@default` trait can only be added to a member if the member was - previously marked as `@required`. This ensures that generated code for the - member remains non-nullable. -- The `@required` trait can only be removed under the following conditions: - - It is replaced with the `@default` trait - - The containing structure is marked with the `@input` trait. - - The member is also marked with the `@clientOptional` trait. -- The `@required` trait can only be added to a member if the member is also - marked with the `@clientOptional` trait. This is useful to correct a model - that errantly omitted the `@required` trait, but the member is actually - required by the service. Adding the `@required` trait to a member but - omitting the `@clientOptional` trait is a breaking change because it - transitions the member from nullable to non-nullable in generated code. -- The `@clientOptional` trait can only be removed from members that are not - marked as `@required`. - -For example, if on _day-1_ the following structure is released: - -``` -structure Message { - @required - title: String - - message: String -} -``` - -On _day-2_, the service team realizes Message does not require a `title`, so -they add the `@default` trait and remove the `@required` trait. The member will -remain non-nullable because clients, servers, and any other deserializer will -now provide a default value for the `title` member if a value is not provided by -the end user. - -``` -structure Message { - @default - title: String - - message: String -} -``` - -Backward compatibility guarantees of the `@default` and `@required` traits -SHOULD be enforced by tooling. For example, smithy-diff in Smithy's reference -implementation will be updated to be aware of these additional backward -compatibility rules. - - -## Guidance on code generation - -Code generated types for structures SHOULD use the `@default` trait and -`@required` traits to provide member accessors that always return non-null -values based on the following ordered rules: - -1. Accessors for members of a structure marked with the `@input` MUST be - nullable. -2. Accessors for members marked as `@clientOptional` MUST be nullable. -3. Accessors for members marked as `@required` SHOULD always return a non-null - value. -4. Accessors for members marked with the `@default` trait SHOULD always return - a non-null value by defaulting missing members with their zero values. -5. All other structure member accessors are considered nullable. - -**Note**: Smithy implementations in languages like TypeScript that do not -provide a kind of constructor or builder to create structures may not be able -to set default values, precluding them from being able to treat `@required` -and `@default` members as non-null. - - -### Guidance on protocol design - -Protocols MAY choose if and how the `@default` trait impacts serialization and -deserialization. However, this proposal offers the following general -best-practices for protocol designers: - -1. Serializing the default zero value of a member marked with the `@default` - trait can lead to unintended information disclosure. For example, consider - a newly introduced structure member marked with the `@default` trait that is - only exposed to customers of a service that are allowlisted into a private - beta. Serializing the zero values of these members could expose the feature - to customers that are not part of the private beta because they would see - the member serialized in messages they receive from the service. -2. Protocol deserialization implementations SHOULD tolerate receiving a - serialized default zero value. This also accounts for older clients that - think a structure member is required, but the service has since transitioned - the member to use the `@default` trait. -3. Client implementations SHOULD tolerate structure members marked as - `@required` that have no serialized value. For example, if a service - migrates a member from `@required` to `@default`, then older clients SHOULD - gracefully handle the zero value of the member being omitted on the wire. - In this case, rather than failing, a client SHOULD set the member value to - its default zero value. Failing to deserialize the structure is a bad - outcome because what the service perceived as a backward compatible change - (i.e., replacing the `@required` trait with the `@default` trait) could - break previously generated clients. - - -## AWS specific changes - -### AWS Protocols - -All existing AWS protocols (that is, `aws.protocols#restJson1`, -`aws.protocols#awsJson1_0`, `aws.protocols#awsJson1_1`, -`aws.protocols#restJson1`, `aws.protocols#restXml`, `aws.protocols#awsQuery`, -and `aws.protocols#ec2Query`) MUST adhere to the following requirements, and -all future AWS protocols SHOULD adhere to the following: - -1. To avoid information disclosure, serializers SHOULD omit the default zero - value of structure members marked with the `@default` trait. -2. Deserializers MUST tolerate receiving the default zero value of a structure - member marked with the `@default` trait. -3. Client deserializers MUST fill in a default zero value for structure members - marked with the `@required` trait that have no serialized value, and the - targeted shape supports zero values. This prevents older clients from - failing to deserialize structures at runtime when the `@required` trait is - replaced with the `@default` trait. - - -### Balancing old and new assumptions - -Smithy comes from a long line of service frameworks built inside Amazon dating -back nearly 20 years. Until this proposal, the required trait was only treated -as server-side input validation. This causes two major issues that impacts the -AWS SDK team's ability to fully use the required trait for code generation: - -1. Many teams did not consider the required trait as something that impacts - output. This means that there are many members in AWS that should be - marked as required but are not, and there are members that are required in - input, but not always present in output. -2. Most teams designed their APIs under the guidance that the required trait - can be removed without breaking their end users. - -We will work to improve the accuracy and consistency of the required trait in -AWS API models where possible, but it will take significant time and effort -from hundreds of different teams within AWS. To accommodate this shift in -modeling changes, we will use the `@clientOptional` trait to influence code -generation and provide AWS SDK specific code generation recommendations. - -The `@clientOptional` trait will be backfilled onto AWS models as needed. -Services with a history of frequently adding or removing the required trait -will apply the `@clientOptional` to every `@required` member. Other AWS models -will only apply the `@clientOptional` trait to members that target structures -or unions. Over time, as models are audited and corrected, we can remove the -`@clientOptional` trait and release improved AWS SDKs. - -Backfilling this trait on AWS models is admittedly be an inconvenience for -developers, but it is less catastrophic than previously generated client code -failing at runtime when deserializing the response of an operation, and it -matches the current reality of how AWS was modeled. While it's true client code -might start failing anyways if a service stops sending output members that were -previously marked as required, it would only fail when the client explicitly -attempts to dereference the value, and this kind of change is generally only -made when a client opts-into some new kind of functionality or workflow. If an -SDK fails during deserialization because a previously required output member -is missing, then customer applications would be completely broken until they -update their SDK, regardless of if the member is used in client code. - - -## Alternatives and trade-offs - -### Don't do anything - -We could keep things as-is, which means we avoid a major version bump in the -Smithy IDL, and we don't require any kind of campaign to apply missing -`@required` traits. This option is low-risk, but does mean that we'll continue -to expose an excessive amount of null values in Smithy codegen. This may become -more problematic as teams are beginning to use Smithy to model data types -outside of client/server interactions. - - -### Remove the `@required` trait - -Another alternative for Smithy is to remove the `@required` trait and make -every structure member nullable. This is essentially how many Smithy code -generators function today, and removing the `@required` trait would codify this -in the specification. However, the `@required` trait still provides value to -services because they have perfect knowledge of what is and is not required, -and therefore can automatically enforce that required properties are sent from -a client. It also provides value in documentation because it defines at that -point in time which members a caller must provide and which members they can -expect in a response. - - -## FAQ - - -### What's the biggest risk of this proposal? - -Service teams not understanding the traits, and then accidentally making -breaking changes to Smithy generated SDKs. Or they potentially need to make a -breaking change in SDKs because they did not understand the traits. We can -mitigate this somewhat through validation and backward compatibility checks. - - -### Is the `@default` trait only used to remove the `@required` trait? - -No. It can be used any time the zero value of a structure member is a -reasonable default or can be treated the same as an omitted value. For example, -if `0` and `null` could communicate the same thing to a service, then using the -`@default` trait makes it easier to use the value because a null check isn't -needed before doing something with the value. - - -### Why not allow for custom default values? - -Allowing a custom default value couples clients and servers and limits a -service's ability to change defaults. It takes a long time for customers to -update to newer versions of generated clients, and some customers never update. -Populating default values in clients would mean that it could take years for a -service team to migrate customers from one default to another. For example, it -might be reasonable to set a default `pageSize` for paginated operations to -1000, but the service team might later realize that the default should have been -500 (for whatever reason). However, only new customers and customers that update -to the newer version of the client will use the updated default. - -Another issue with custom default values is that languages that do not provide -constructors or Smithy implementations that do not have constructors like -JavaScript cannot set a custom default value when a type is initialized. This -means that these languages either need to forbid creating structs through normal -assignment and provide factory methods, they need to forgo non-nullable members -altogether, or they need to require end-users to assign default values when -creating the type (i.e., to satisfy TypeScript structural typing). - -An alternative approach is to allow custom default values, but default values -are never serialized. This allows defaults to evolve on the service without -updating clients. However, this presents inconsistent views between clients, -servers, and other parties as to what a structure member contains. For example, -if a service sets the default value of a string to "A" on day 1, and the client -also sees that the default value is "A", omitting "A" on the wire works and -there are no surprises. However, if the service updates the default value to "B" -on day 2, and the client has not yet updated, the service will omit "B" on the -wire, the client will assume the value is "A". If the client then sends this -structure back to the service, it will omit "A", and the service will assume -"B". - -One final issue with custom default values: removing the `@required` trait and -replacing it with the `@default` trait works because when a client is -deserializing a structure, if what it assumed is a `@required` member is -missing, the client knows that the member was transitioned by the service to -optional and can set the member to a deterministic default, zero value. If -the default value is variable, then a client cannot reasonably populate a valid -default value and would need to assume the zero value is the default. This works -for deserializing the structure, but if the structure were to be round-tripped -and sent back to the service, the client would see that the member is -`@required`, and it would send the assumed zero value instead of the actual -default value of the member. This is the primary reason why allowing custom -default values that are only honored by the service and not by clients isn't -possible to support. - - -### How will this impact smithy-openapi? - -We will start using the `default` property of OpenAPI and JSON Schema. For -example: - -``` -{ - "components": { - "schemas": { - "Cat": { - { - "type": "object", - "properties": { - "huntingSkill": { - "type": "string", - "default": "" - } - }, - "required": [ - "huntingSkill" - ] - } - ] - } - } - } -} -``` - - -### How many AWS members used in output marked as required? - -As of March 17, 2021, 4,805 members. - - -### How often has the `@required` trait been removed from members in AWS? - -The required trait has been removed from a structure member 618 different times -across 105 different services. Of those removals, 75 targeted structures, 350 -targeted strings, and the remainder targeted lists, maps, and other simple -shapes. - - -### How often has the `@required` trait been added to members in AWS? - -The required trait has been added to a member 9 different times across 5 -different services. - - -### What are the `@input` and `@output` traits? - -See https://github.com/awslabs/smithy/blob/main/designs/operation-input-output-and-unit-types.md - - -## Updates - -* 2022-06-08 - * Remove default zero values from structures. Assigning zero values to - structures adds complexity to serializers that try to omit members set - to their zero values (they would have needed to recursively inspect - structure member values or generate some kind of "isZeroValue" flag). - Rather than add default zero values to structures to aid in migrating - required structures to default structures, services can choose to apply - the `@clientOptional` trait to the member to ensure that clients generate - the member as optional, and the service reserves the right to remove the - `@required` trait in the future. - * Rename `@nullable` to `@clientOptional`. `@nullable` caused confusion; - some assumed it meant that a member accepts explicit null values when it - actually is just intended to make clients generate the member as optional. -* 2022-05-04 - * Move `aws.api#clientOptional` to `smithy.api#nullable`. In order - for diff tools to understand backward compatible changes, this trait has to - be a core part of Smithy that tooling can reason about. The trait has - utility outside of AWS too as it allows any model to define their own - backward compatibility guarantees for a required member and does not - require teams to ignore Smithy's built-in diff support that checks for - allowed `@required` changes. - * Remove Smithy 2.0 traits and semantics from 1.0. Adding `@default` and - allowing `@clientOptional` in 1.0 made an already complex nullability - story impossibly complex. `@default` and `@clientOptional` is now only be - allowed in Smithy 2.0. diff --git a/designs/defaults-and-model-evolution.md b/designs/defaults-and-model-evolution.md new file mode 100644 index 00000000000..fb20eb28d15 --- /dev/null +++ b/designs/defaults-and-model-evolution.md @@ -0,0 +1,770 @@ +# Defaults and Model Evolution + +* **Author**: Michael Dowling +* **Created**: 2022-06-22 + + +## Abstract + +This document describes a fundamental change to the way Smithy describes +structure member optionality. By adding a `@default` trait to structure +members, code generators can generate non-optional accessors (or getters) for +members marked as `@default` or `@required` without sacrificing model +evolution (with some caveats for backward compatibility). While this proposal +is not AWS-specific, the practical impact of it is that when implemented, it +will convert thousands of optional property accessors to non-optional in +programming languages that model optionality in their type systems. + +This proposal requires breaking changes to Smithy and proposes that the model +move to version 2.0. + + +## Motivation + +Most structure member accessors generated from Smithy 1.0 models return +optional values. As new languages like Rust and Kotlin that explicitly model +optionality in their type systems adopt Smithy, excessive optionality in +generated code becomes burdensome to end-users because they need to call +methods like `.unwrap()` on everything. Generating every structure member as +optional makes it hard for customers to know when it is safe to dereference a +value and when it will result in an error. Adding the ability to control when +a value is optional vs when a value is always present provides an ergonomic and +safety benefit to end users of code generated types. + +For example, after this proposal is implemented, the following Rust code: + +```rust +println!("Lexicons:"); +let lexicons = resp.lexicons.unwrap_or_default(); +for lexicon in &lexicons { + println!( + " Name: {}", + lexicon.name.as_deref().unwrap_or_default() + ); + println!( + " Language: {:?}\n", + lexicon + .attributes + .as_ref() + .map(|attrib| attrib + .language_code + .as_ref() + .expect("languages must have language codes")) + .expect("languages must have attributes") + ); +} +println!("\nFound {} lexicons.\n", lexicons.len()); +``` + +Can be simplified to: + +```rust +println!("Lexicons:"); +for lexicon in &resp.lexicons { + println!(" Name: {}", lexicon.name); + println!(" Languages: {:?}\n", + lexicon.attributes.map(|attrib| attrib.language_code) + ); +} +println!("\nFound {} lexicons.\n", resp.lexicons.len()); +``` + + +## Background + +Optionality in Smithy IDL 1.0 is controlled in the following ways: + +1. Map keys, values in sets, lists with `@uniqueItems`, and unions cannot + contain optional values. This proposal does not change this behavior. +2. List and map values can only contain optional values when marked with the + `@sparse` trait. These kinds of collections rarely need to contain optional + members. This proposal does not change this behavior. +3. The optionality of structure members in Smithy today is resolved using the + following logic: + 1. If the member is marked with `@box`, it's optional. + 2. If the shape targeted by the member is marked with `@box`, it's optional. + 3. If the shape targeted by the member is a byte, short, integer, long, + float, double, or boolean, it's non-optional because it has a default + zero value. + 4. All other members are considered optional. + + +### Why was the `@required` trait unreliable for codegen? + +Removing the `@required` trait from a structure member has historically been +considered backwards compatible in Smithy because it is loosening a restriction. +The `@required` trait in Smithy 1.0 is categorized as a constraint trait and +only used for validation rather than to influence generated types. This gives +service teams the flexibility to remove the required trait as needed without +breaking generated client code. The `@required` trait might be removed if new +use cases emerge where a member is only conditionally required, and more +rarely, it might be added if the service team accidentally omitted the trait +when the service initially launched. + +For context, as of May 2021, the `@required` trait has been removed from a +structure member in 105 different AWS services across 618 different members. +Encoding the `@required` trait into generated types would have made changing a +member from required to optional a breaking change to generated code, which is +something most services try to avoid given its frequency and the cost of +shipping a major version of a service and client. While this data is +AWS-specific, it's indicative of how often disparate teams that use Smithy +(or its predecessors within Amazon) sought to update their APIs to make members +optional. + + +## Goals and non-goals + +**Goals** + +1. Reduce the amount of optionality in code generated from Smithy models. +2. Allow for similar model evolution guarantees that exist today like being + able to backward compatibly remove the `@required` trait from a structure + member without breaking previously generated clients. +3. Maintain Smithy's protocol-agnostic design. Protocols should never influence + how types are generated from Smithy models; they're only intended to change + how types are serialized and deserialized. In practice, this means that + features of a protocol that extend beyond Smithy's metamodel and definitions + of structure member optionality should not be exposed in code generated + types that represent Smithy shapes. + + +## High-level summary + +This proposal: + +1. Introduces a `@default` trait +2. Introduces a `@clientOptional` trait +3. Removes the `@box` trait +4. Removes the `Primitive*` shapes from the prelude +5. Make the optionality of a structure completely controlled by members rather + than the shape targeted by a member + +The `@default` trait initializes a structure member with a value (note that IDL +v2 introduces syntactic sugar to structure members to define a `@default` +trait). + +``` +structure Message { + @required + title: String + + message: String = "Hello" +} +``` + +In the above example: + +- `title` is marked as `@required`, so it is non-optional in generated code. +- `message` is also non-optional in generated code and is assigned a default + value of `"Hello"` when not explicitly provided. This makes it easier to use + `title` and `message` in code without having to first check if the value is + `null`. In many languages, one can call `message.message.size()` without + first checking if the value is non-optional. + +If the `title` member ever needs to be made optional, the `@required` trait +can be replaced with the `@default` trait: + +``` +structure Message { + title: String = "" + message: String = "" +} +``` + +With the above change, codegen remains the same: both values are non-optional. +However, if `title` is not set in client code, server-side validation for the +type will _not_ fail because `title` has a default value of `""`. + + +## Proposal overview + +Introduce a 2.0 of Smithy models that simplifies optionality by moving +optionality controls from shapes to structure members. Smithy IDL 2.0 will: + +1. Add a `@default` trait that can target structure members to assign a default + value. Whether a member has a default value is no longer controlled based on + the shape targeted by a member, localizing this concern to members. This + makes optionality of a member easier to understand for both readers and + writers. +2. Add a `@clientOptional` trait that can target structure members. This trait + is essentially a more constrained and better named `@box` trait. The primary + use case for this trait is to apply it to members also marked as `@required` + to force non-authoritative code generators like clients to treat the member + as optional. The service reserves the right to remove the `@required` trait + from a member without replacing it with a `@default` trait without a major + version bump of the service. +3. Remove the `@box` trait from the Smithy 2.0 prelude and fail to load models + that contain the `@box` trait. +4. Remove the `PrimitiveBoolean`, `PrimitiveShort`, `PrimitiveInteger`, + `PrimitiveLong`, `PrimitiveFloat`, and `PrimitiveDouble` shapes from the + Smithy 2.0 prelude. IDL 2.0 models will fail if they use these shapes. +5. Update the Smithy IDL 2.0 model loader implementation to be able to load + Smithy 1.0 models alongside Smithy 2.0 models. + 1. Inject an appropriate `@default` trait on structure members that targeted + shapes with a default zero value and were not marked with the `@box` + trait. + 2. Replace the `@box` trait with `@clientOptional` on structure members. + 3. Remove the `@box` trait from non-members. + 4. Rewrite members that target one of the removed Primitive* shapes to + target the corresponding non-primitive shape in the prelude (for example, + `PrimitiveInteger` is rewritten to target `Integer`). + + +## `@default` trait + +The `@default` trait can be applied to structure members to provide a default +value. + +The following example defines a structure with a "language" member that has a +default value: + +``` +structure Message { + @required + title: String + + language: Language = "en" +} + +enum Language { + EN = "en" +} +``` + +The above example uses syntactic sugar to apply the `@default` trait. It is +semantically equivalent to: + +``` +structure Message { + @required + title: String + + @default("en") + language: Language +} +``` + +The `@default` trait is defined in Smithy as: + +``` +/// Provides a structure member with a default value. +@trait( + selector: "structure > member :test(> :is(simpleType, collection, map))", + // The default trait can never be removed. It can only be added if the + // member was previously marked as required or is required. + breakingChanges: [ + {change: "remove"}, + {change: "update", severity: "DANGER", message: "Default values should only be changed when absolutely necessary."} + ] +) +document default +``` + + +### Default value constraints + +The `@default` trait accepts a document type. The value of the trait MUST +be compatible with the shape targeted by the member and any applied constraint +traits (for example, values for numeric types MUST be numbers that fit within +the targeted type and match any `@length` constraints, string types match any +`@pattern` or `@length` traits, etc). + +The following shapes have restrictions on their default values: + +* enum: can be set to any valid string _value_ of the enum. +* intEnum: can be set to any valid integer _value_ of the enum. +* document: can be set to `true`, `false`, string, empty list, or empty map. +* list/set: can only be set to an empty list. +* map: can only be set to an empty map. +* structure: no default value. +* union: no default value. + + +### Impact on API design + +The `@default` trait SHOULD NOT be used for partial updates or patch style +operations where it is critical to differentiate between omitted values and +explicitly set values. Assigning default values is typically something that +occurs during deserialization, and as such, it is impossible for a server to +differentiate between whether a property was set to its default value or if a +property was omitted. + +To help guide API design, built-in validation will be added to Smithy to detect +and warn when `@default` members are detected in the top-level input of +operations that start with `Update`, operation bound to the `update` lifecycle +of a resource, or operations that use the `PATCH` HTTP method. + +For example, the following model: + +``` +$version: "2" +namespace smithy.examnple + +operation UpdateUser { + input: UpdateUserInput +} + +structure UpdateUserInput { + username: String = "" +} +``` + +Would emit a warning similar to the following: + +``` +WARNING: smithy.example#UpdateUserInput$userName (DefaultValueInUpdate) + @ /path/to/main.smithy + | + 4 | operation UpdateUser { + | ^ + = This update style operation has top-level input members marked with the + @default trait. It will be impossible to tell if the member was omitted + or explicitly provided. Affected members: [UpdateUserInput$username]. +``` + + +### Updating default values + +The default value of a member SHOULD NOT be changed. However, it MAY be +necessary in extreme cases to change a default value if changing the default +value addresses a customer-impacting issue or availability issue for a service. +Changing default values can result in parties disagreeing on the default value +of a member because they are using different versions of the same model. + + +### Default value serialization and deserialization + +When deserializing a structure, a reader MUST set the member to its default +value if the member is missing. Outside of deserialization, there MUST NOT be +any discernible difference between an explicitly serialized member or a member +that was assigned the default value. If such a distinction is needed, then the +`@default` trait is inappropriate. + +Authoritative model consumers like servers SHOULD always serialize default +values to remove any ambiguity about the value of the most up to default +value. When persisting data structures, regardless of if they are persisted by +a client or server, default values SHOULD be persisted to ensure that model +changes do not impact previously serialized values. + + +#### `@default` and `@required` + +A member that is marked as both `@required` and `@default` indicates that +the member MUST be serialized even if the member is set to the default value. + +``` +structure Message { + // this member must be serialized + @required + title: String = "Hello" +} +``` + + +#### Default values and clients + +To allow default values to be controlled by servers, clients SHOULD NOT +serialize default values unless the member is marked as `@required` or if +the value of the member is explicitly set by the client to the default value. +This implies that clients need to implement "presence tracking" of defaulted +members. + +If a mis-configured server fails to serialize a value for a required member, +to avoid downtime, clients MAY attempt to fill in an appropriate default value +for the member: + +* `@default`: If the member has the `@default` trait, use its value +* boolean: false +* numbers: 0 +* timestamp: 0 seconds since the Unix epoch +* string and blob: "" +* document: null +* list: [] +* map: {} +* enum, intEnum, union: The unknown variant. These types SHOULD define an + unknown variant to account for receiving unknown members. +* structure: {} if possible, otherwise a deserialization error. +* union: a deserialization error. + + +## `@clientOptional` trait + +For cases when a service unsure if a member will be required forever, the +member can be marked with the `@clientOptional` trait to ensure that +non-authoritative consumers of the model like clients treat the member as +optional. The `@required` trait can be backward compatibly removed from a +member marked as `@clientOptional` (and not replaced with the `@default` +trait). This causes the `@required` trait to function as server-side validation +rather than something that changes generated code. + +Structure members in Smithy are automatically considered optional. For example, +the following structure: + +``` +structure Foo { + baz: String +} +``` + +Is equivalent to the following structure: + +``` +structure Foo { + @clientOptional + baz: String +} +``` + +The primary use case of the `@clientOptional` trait is to indicate that while a +member is _currently_ defined as `@required`, the service reserves the right to +remove the `@required` trait and make the member optional in the future. + +For example, the `@required` trait on `foo` in the following structure is +considered a validation constraint rather than a type refinement trait: + +``` +structure Foo { + @required + @clientOptional + foo: String +} +``` + +The `@clientOptional` trait is defined in Smithy as: + +``` +@trait(selector: "structure > member") +structure clientOptional {} +``` + +The `@clientOptional` trait applied to a member marked with the `@default` +trait causes non-authoritative generators to ignore the `@default` trait: + +``` +structure Message { + @clientOptional + title: String = "" +} +``` + + +### @input structures and @clientOptional + +Members of a structure marked with the `@input` trait are implicitly considered +to be marked with `@clientOptional`. The `@input` trait special-cases a +structure as the input of a single operation that cannot be referenced in any +structures marked with the `@input` have more relaxed backward compatibility +guarantees. It is backward compatible to remove the `@required` trait from +top-level members of structures marked with the `@input` trait, and the +`@required` trait does not need to be replaced with the `@default` trait +(though this is allowed as well). This gives service teams the ability to +remove the `@required` trait from top-level input members and loosen +requirements without risking breaking previously generated clients. + +The practical implication of this backward compatibility affordance is that +code generated types for members of an `@input` structure SHOULD all be +considered optional regardless of the use of `@required` or `@default`. +Generators MAY special-case members that serve as resource identifiers to be +non-nullable because those members can never remove the `@required` trait. +Not observing these optionality affordances runs the risk of previously +generated code breaking when a model is updated in the future. + +Organizations that want stricter optionality controls over inputs can choose to +not use the `@input` trait. + + +## Backward compatibility rules + +The key principle to consider is if adding or removing a trait will change the +optionality of a member in generated code. If it does, then the change is not +backward compatible. Backward compatibility rules of the `@default`, +`@required`, and `@clientOptional` traits are as follows: + +- The `@default` trait can never be removed from a member. +- The value of the `@default` trait SHOULD NOT be changed unless absolutely + necessary. +- The `@default` trait can only be added to a member if the member is or was + previously marked as `@required` or `@clientOptional`. This ensures that + generated code for the member remains non-optional. +- The `@required` trait can only be removed under the following conditions: + - It is replaced with the `@default` trait + - The containing structure is marked with the `@input` trait. + - The member is also marked with the `@clientOptional` trait. +- The `@required` trait can only be added to a member if the member is also + marked with the `@clientOptional` trait. This is useful to correct a model + that errantly omitted the `@required` trait, but the member is actually + required by the service. Adding the `@required` trait to a member but + omitting the `@clientOptional` trait is a breaking change because it + transitions the member from optional to non-optional in generated code. +- The `@clientOptional` trait can only be removed from members that are not + marked as `@required` or `@default`. + +For example, if on _day-1_ the following structure is released: + +``` +structure Message { + @required + title: String + + message: String +} +``` + +On _day-2_, the service team realizes Message does not require a `title`, so +they add the `@default` trait and remove the `@required` trait. The member will +remain non-optional because clients, servers, and any other deserializer will +now provide a default value for the `title` member if a value is not provided by +the end user. + +``` +structure Message { + title: String = "" + + message: String +} +``` + +Backward compatibility guarantees of the `@default` and `@required` traits +SHOULD be enforced by tooling. For example, smithy-diff in Smithy's reference +implementation will be updated to be aware of these additional backward +compatibility rules. + + +## Guidance on code generation + +Code generated types for structures SHOULD use the `@default` trait and +`@required` trait to provide member accessors that always return non-optional +values based on the following ordered rules: + +1. Accessors for members of a structure marked with the `@input` SHOULD be + optional. +2. Accessors for members marked as `@clientOptional` MUST be optional. +3. Accessors for members marked as `@required` SHOULD always return a + non-optional value. +4. Accessors for members marked with the `@default` trait SHOULD always return + a non-optional value by defaulting missing members. +5. All other structure member accessors are considered optional. + +**Note**: Smithy implementations in languages like TypeScript that do not +provide a kind of constructor or builder to create structures may not be able +to set default values, precluding them from being able to treat `@required` +and `@default` members as non-optional. + + +## AWS specific changes + +Smithy comes from a long line of service frameworks built inside Amazon dating +back nearly 20 years. Until this proposal, the required trait was only treated +as server-side input validation. This causes two major issues that impacts the +AWS SDK team's ability to fully use the required trait for code generation: + +1. Many teams did not consider the required trait as something that impacts + output. This means that there are many members in AWS that should be + marked as required but are not, and there are members that are required in + input, but not always present in output. +2. Most teams designed their APIs under the guidance that the required trait + can be removed without breaking their end users. + +We will work to improve the accuracy and consistency of the required trait in +AWS API models where possible, but it will take significant time and effort +from hundreds of different teams within AWS. To accommodate this shift in +modeling changes, we will use the `@clientOptional` trait to influence code +generation and provide AWS SDK specific code generation recommendations. + +`@clientOptional`, `@default`, and `@required` traits will be backfilled onto +AWS models as needed. Services with a history of frequently adding or removing +the required trait will apply the `@clientOptional` to every `@required` +member. Other AWS models will only apply the `@clientOptional` trait to members +that target structures or unions. Over time, as models are audited and +corrected, we can remove the `@clientOptional` trait and release improved +AWS SDKs. + +Applying the `@clientOptional` trait on AWS models is admittedly be an +inconvenience for developers, but it is less catastrophic than previously +generated client code failing at runtime when deserializing the response of an +operation, and it matches the current reality of how AWS was modeled. While +it's true client code might start failing anyways if a service stops sending +output members that were previously marked as required, it would only fail when +the client explicitly attempts to dereference the value, and this kind of +change is generally only made when a client opts-into some new kind of +functionality or workflow. If an SDK fails during deserialization because a +previously required output member is missing, then customer applications would +be completely broken until they update their SDK, regardless of if the member +is used in client code. + + +## Alternatives and trade-offs + +### Don't do anything + +We could keep things as-is, which means we avoid a major version bump in the +Smithy IDL, and we don't require any kind of campaign to apply missing +`@required` traits. This option is low-risk, but does mean that we'll continue +to expose an excessive amount of optional values in Smithy codegen. This may +become more problematic as teams are beginning to use Smithy to model data types +outside of client/server interactions. + + +### Make everything optional + +Another alternative for Smithy is to remove the `@required` trait and make +every structure member optional. This is essentially how many Smithy code +generators function today, and removing the `@required` trait would codify this +in the specification. However, the `@required` trait still provides value to +services because they have perfect knowledge of what is and is not required, +and therefore can automatically enforce that required properties are sent from +a client. It also provides value in documentation because it defines at that +point in time which members a caller must provide and which members they can +expect in a response. + + +### Only support default zero values instead of custom defaults + +Only supporting the default zero value for structure members has some distinct +advantages, but also disadvantages. If we only supported default zero values, +then it would be possible to omit far more default values on the wire, because +any time a required member is missing, a deserializer can safely assume the +member transitioned from `@required` to `@default` and fill in the default +zero value. This helps to avoid unintended information disclosure and can +reduce payload sizes. The other major benefit is that presence tracking isn't +needed because default values can never change. + +However, only supporting default zero values means that service teams will need +to add confusing enum values like `""` and `0` to enums and intEnums. It also +doesn't match the reality that default values are already ubiquitous; for +example, there likely more than 1,000 members in AWS that currently have +default values that are only captured in API documentation strings. If default +values are explicitly modeled, then they are easier to audit in API review +processes to ensure that a service team understands the implications of +changing a default value. + + +### Always send default values + +Always sending default values would be a big simplification to clients because +it removes presence tracking. Always sending defaults would make it explicit +as to what each party thinks the default value is. However, this would remove +the ability for a service to change a default value if ever needed. While the +service can change the default value and wait their consumers upgrade their +clients the change will take effect, there is usually an extremely long-tail of +users that will not upgrade to the latest version. If a service needs to change +a default value due to an operational or customer impacting issue, then that +change needs to take effect across all currently deployed clients as soon as +possible, hence the need for presence tracking in clients to only send default +values if they are explicitly set. + + +## FAQ + + +### What's the biggest risk of this proposal? + +Service teams not understanding the traits, and then accidentally making +breaking changes to Smithy generated SDKs. Or they potentially need to make a +breaking change in SDKs because they did not understand the traits. We can +mitigate this somewhat through validation and backward compatibility checks. + + +### Is the `@default` trait only used to remove the `@required` trait? + +No. It can be used any time a member has a reasonable default value. + + +### When should the `@default` trait not be used? + +The `@default` trait should not be used if the value used for a member is +dynamic (that is derived from other members, generated from other context, +etc). + + +### Why not allow deeply nested default values? + +The `@default` trait does not provide a default value for structures and unions, +does not allow the default values for lists/sets/maps to be anything other than +empty lists/sets/maps, and does not allow non-empty lists or maps for document +types. This limitation is primarily to simplify code generation. For example, +constant values do not need to be created for default complex types and copied +each time a structure is created. + +It is rare that a default other than an empty list or map is needed. In the +case of a structure or union, it's added complexity for little gain; if a +structure could potentially define a sensible default value, then just set the +member to that sensible value. Unions can easily add a new variant to represent +the lack of a value: + +``` +union MyUnion { + None: Unit +} +``` + + +### I need to add a member to output that is always present. How? + +Even if the structure member is only ever seen by clients in output, it +is still a breaking change to add a member with the `@required` trait. For +example, previously written unit tests that create this type would now fail to +build if a new required member is added. + +In these cases, use the `@default` trait even if the member is initialized with +a kind of zero value that will never be observed by clients because the server +will provide an actual non-zero value. + + +### How will this impact smithy-openapi? + +We will start using the `default` property of OpenAPI and JSON Schema. For +example: + +``` +{ + "components": { + "schemas": { + "Cat": { + { + "type": "object", + "properties": { + "huntingSkill": { + "type": "string", + "default": "" + } + }, + "required": [ + "huntingSkill" + ] + } + ] + } + } + } +} +``` + +Because Amazon API Gateway does not support the `default` property (as of +June, 2022), it is automatically stripped when generating OpenAPI models for +Amazon API Gateway. If `default` support is added, it could be something +enabled via an API Gateway specific opt-in flag. + + +### How many AWS members used in output marked as required? + +As of March 17, 2021, 4,805 members. + + +### How often has the `@required` trait been removed from members in AWS? + +The required trait has been removed from a structure member 618 different times +across 105 different services. Of those removals, 75 targeted structures, 350 +targeted strings, and the remainder targeted lists, maps, and other simple +shapes. + + +### How often has the `@required` trait been added to members in AWS? + +The required trait has been added to a member 9 different times across 5 +different services. + + +### What are the `@input` and `@output` traits? + +See https://github.com/awslabs/smithy/blob/main/designs/operation-input-output-and-unit-types.md diff --git a/designs/enum-shapes.md b/designs/enum-shapes.md index 8f16a86828a..ac9792e70d4 100644 --- a/designs/enum-shapes.md +++ b/designs/enum-shapes.md @@ -2,7 +2,7 @@ * **Authors**: Michael Dowling, Jordon Phillips * **Created**: 2022-02-14 -* **Last updated**: 2022-02-14 +* **Last updated**: 2022-06-22 ## Abstract @@ -49,60 +49,31 @@ enum Suit { Each value listed in the enum is a member that implicitly targets `smithy.api#Unit`. The string representation of an enum member defaults to the member name. The string representation can be customized by applying the -`@enumValue` trait. +`@enumValue` trait. We will introduce syntactic sugar to assign the +`@enumValue` trait to enums and intEnums. ``` enum Suit { - @enumValue("diamond") - DIAMOND - - @enumValue("club") - CLUB - - @enumValue("heart") - HEART - - @enumValue("spade") - SPADE -} -``` - -Enums do not support aliasing; all values MUST be unique. - -#### enum default values - -The default value of the enum shape is an empty string "", regardless of if -the enum defines a member with a value of "". - -``` -structure GetFooInput { - @default - suit: Suit + DIAMOND = "diamond" + CLUB = "club" + HEART = "heart" + SPADE = "spade" } ``` -To account for this, enums MAY define a default member by setting the -`@enumDefault` trait on a member: +The above enum definition is exactly equivalent to: ``` enum Suit { - @enumDefault - UNKNOWN - - @enumValue("diamond") - DIAMOND - - @enumValue("club") - CLUB - - @enumValue("heart") - HEART - - @enumValue("spade") - SPADE + DIAMOND = "diamond" + CLUB = "club" + HEART = "heart" + SPADE = "spade" } ``` +Enums do not support aliasing; all values MUST be unique. + #### enum is a specialization of string Enums are considered open, meaning it is a backward compatible change to add @@ -127,11 +98,11 @@ Every enum shape MUST define at least one member. #### enum members always have a value -If an enum member doesn't have an explicit `@enumValue` or `@enumDefault` trait, -an `@enumValue` trait will be automatically added to the member where the trait -value is the member's name. This means that enum members that have neither the -`@enumValue` nor the `@enumDefault` trait are indistinguishable from enum members -that have the `@enumValue` trait explicitly set. +If an enum member doesn't have an explicit `@enumValue` trait, an `@enumValue` +trait will be automatically added to the member where the trait value is the +member's name. This means that enum members that have no `@enumValue` trait +are indistinguishable from enum members that have the `@enumValue` trait +explicitly set. The following model: @@ -148,20 +119,15 @@ Is equivalent to: ``` enum Suit { - @enumValue("DIAMOND") - DIAMOND - - @enumValue("CLUB") - CLUB - - @enumValue("HEART") - HEART - - @enumValue("SPADE") - SPADE + DIAMOND = "DIAMOND" + CLUB = "CLUB" + HEART = "HEART" + SPADE = "SPADE" } ``` +The value of an enum cannot be set to an empty string. + ### intEnum shape An intEnum is used to represent an enumerated set of integer values. The @@ -170,61 +136,15 @@ integer value. The following example defines an intEnum shape: ``` intEnum FaceCard { - @enumValue(1) - JACK - - @enumValue(2) - QUEEN - - @enumValue(3) - KING - - @enumValue(4) - ACE - - @enumValue(5) - JOKER -} -``` - -#### intEnum default values - -The default value of the intEnum shape is 0, regardless of if the enum defines -a member with a value of 0. - -``` -structure GetFooInput { - @default - suit: Suit -} -``` - -intEnums MAY define a member to represent the default value of the shape by -setting the `@enumDefault` trait on a member: - -``` -intEnum FaceCard { - @enumDefault - UNKNOWN - - @enumValue(1) - JACK - - @enumValue(2) - QUEEN - - @enumValue(3) - KING - - @enumValue(4) - ACE - - @enumValue(5) - JOKER + JACK = 1 + QUEEN = 2 + KING = 3 + ACE = 4 + JOKER = 5 } ``` -#### intEnum is a specialization of integer +#### intEnum is a specialization of integer Like enums, intEnums are considered open, meaning it is a backward compatible change to add new members. Previously generated clients MUST NOT fail when @@ -289,7 +209,7 @@ intEnum and enum shape, client implementations can reliably receive and round-trip unknown enum values because the unknown value is wire-compatible with an integer or enum. -### Why do enums contain members but you can’t define the shape they target? +### Why do enums contain members, but you can’t define the shape they target? Every value of an enum and intEnum is considered a member so that they have a shape ID and can have traits, but the shape targeted by each enum member is diff --git a/docs/smithy/lexer.py b/docs/smithy/lexer.py index 35b27c6f7fe..2d1239e4678 100644 --- a/docs/smithy/lexer.py +++ b/docs/smithy/lexer.py @@ -70,6 +70,7 @@ class SmithyLexer(RegexLexer): (r'"', String.Double, "string"), (r"[:,]+", Punctuation), (r"\s+", Whitespace), + (r"=", Punctuation) ], "textblock": [ (r"\\(.|$)", String.Escape), diff --git a/docs/source/1.0/guides/converting-to-openapi.rst b/docs/source/1.0/guides/converting-to-openapi.rst index 5c2a5f126a2..3c09b445f7a 100644 --- a/docs/source/1.0/guides/converting-to-openapi.rst +++ b/docs/source/1.0/guides/converting-to-openapi.rst @@ -1293,6 +1293,14 @@ Other traits that influence API Gateway .. seealso:: See :ref:`authorizers` +Amazon API Gateway limitations +============================== + +The ``default`` property in OpenAPI is not currently supported by Amazon +API Gateway. The ``default`` property is automatically removed from OpenAPI +models when they are generated for Amazon API Gateway. + + ------------------------------- Converting to OpenAPI with code ------------------------------- diff --git a/docs/source/1.0/guides/migrating-idl-1-to-2.rst b/docs/source/1.0/guides/migrating-idl-1-to-2.rst index df44d9437f0..ae391ebdf5e 100644 --- a/docs/source/1.0/guides/migrating-idl-1-to-2.rst +++ b/docs/source/1.0/guides/migrating-idl-1-to-2.rst @@ -72,7 +72,7 @@ your models. .. seealso:: - :ref:`structure-nullability` + :ref:`structure-optionality` Replace Primitive prelude shape targets @@ -114,8 +114,7 @@ Needs to be updated to: .. code-block:: smithy structure User { - @default - name: String + name: String = "" } @@ -160,8 +159,7 @@ Needs to be updated to: namespace smithy.example structure OptionalStream { - @default - payload: StreamingBlob + payload: StreamingBlob = "" } structure RequiredStream { @@ -455,15 +453,8 @@ can be updated to: namespace smithy.example enum Suit { - @enumValue("diamond") - DIAMOND - - @enumValue("club") - CLUB - - @enumValue("heart") - HEART - - @enumValue("spade") - SPADE + DIAMOND = "diamond" + CLUB = "club" + HEART = "heart" + SPADE = "spade" } diff --git a/docs/source/1.0/guides/model-linters.rst b/docs/source/1.0/guides/model-linters.rst index ad5b52824c5..53c43c6e169 100644 --- a/docs/source/1.0/guides/model-linters.rst +++ b/docs/source/1.0/guides/model-linters.rst @@ -610,9 +610,9 @@ Rationale guarantees of the ``@required`` trait. For example, it is considered backward compatible to remove the ``@required`` trait from a member and replace it with the ``@default`` trait. However, this isn't possible for - members that target structure or union shapes because they have no zero - value. The risk associated with such members may be unacceptable for some - services. + members that target structure or union shapes because they can have no + default value. The risk associated with such members may be unacceptable + for some services. Default severity ``DANGER`` diff --git a/docs/source/1.0/spec/aws/aws-core.rst b/docs/source/1.0/spec/aws/aws-core.rst index 9c7566455d8..9bd44d76796 100644 --- a/docs/source/1.0/spec/aws/aws-core.rst +++ b/docs/source/1.0/spec/aws/aws-core.rst @@ -1186,33 +1186,16 @@ setting the ``validationMode`` input property to "ENABLED". content: Blob } - @enum([ - { - value: "CRC32C", - name: "CRC32C" - }, - { - value: "CRC32", - name: "CRC32" - }, - { - value: "SHA1", - name: "SHA1" - }, - { - value: "SHA256", - name: "SHA256" - } - ]) - string ChecksumAlgorithm + enum ChecksumAlgorithm { + CRC32C + CRC32 + SHA1 + SHA256 + } - @enum([ - { - value: "ENABLED", - name: "ENABLED" - } - ]) - string ValidationMode + enum ValidationMode { + ENABLED + } The following trait, which does not define request or response checksum diff --git a/docs/source/1.0/spec/aws/aws-default-zero-value.rst.template b/docs/source/1.0/spec/aws/aws-default-zero-value.rst.template index 8e57e6b4137..18e8a464e2c 100644 --- a/docs/source/1.0/spec/aws/aws-default-zero-value.rst.template +++ b/docs/source/1.0/spec/aws/aws-default-zero-value.rst.template @@ -1,9 +1,9 @@ -#. To avoid information disclosure, serializers SHOULD omit the default zero - value of structure members marked with the :ref:`default-trait`. -#. Deserializers MUST tolerate receiving the default zero value of a structure - member marked with the :ref:`default-trait`. -#. Client deserializers MUST fill in a default zero value for structure members - marked with the :ref:`required-trait` that have no serialized value, and the - targeted shape supports zero values. This prevents older clients from - failing to deserialize structures at runtime when the :ref:`required-trait` - is replaced with the :ref:`default-trait`. +#. To avoid information disclosure, serializers SHOULD omit the default + value of structure members that are marked with the :ref:`internal-trait`. +#. Deserializers MUST tolerate receiving the default value of a structure + member. +#. Client deserializers SHOULD fill in a default zero value for structure + members marked with the :ref:`required-trait` that have no serialized + value, and the targeted shape supports zero values. While the server is + in error, it does allow clients to function in the event a service is + miconfigured. diff --git a/docs/source/1.0/spec/aws/customizations/s3-customizations.rst b/docs/source/1.0/spec/aws/customizations/s3-customizations.rst index e4229f3cde0..78c69c70293 100644 --- a/docs/source/1.0/spec/aws/customizations/s3-customizations.rst +++ b/docs/source/1.0/spec/aws/customizations/s3-customizations.rst @@ -150,6 +150,8 @@ Consider the following *abridged* model of S3's ``GetBucketLocation`` operation: .. code-block:: smithy + $version: "2.0" + use aws.customizations#s3UnwrappedXmlOutput @http(uri: "/GetBucketLocation", method: "GET") @@ -165,10 +167,9 @@ Consider the following *abridged* model of S3's ``GetBucketLocation`` operation: LocationConstraint: BucketLocationConstraint } - @enum([ - { value: "us-west-2", name: "us_west_2" } - ]) - string BucketLocationConstraint + enum BucketLocationConstraint { + us_west_2: "us-west-2" + } Since this operation is modeled with ``@s3UnwrappedXmlOutput``, an Amazon S3 client should expect the response from S3 to be unwrapped as shown below: diff --git a/docs/source/1.0/spec/core/idl.rst b/docs/source/1.0/spec/core/idl.rst index f1e92516eba..154c647de96 100644 --- a/docs/source/1.0/spec/core/idl.rst +++ b/docs/source/1.0/spec/core/idl.rst @@ -189,23 +189,24 @@ The Smithy IDL is defined by the following ABNF: :/ "bigDecimal" / "timestamp" enum_shape_statement :`enum_type_name` `ws` `identifier` [`mixins`] `ws` `enum_shape_members` enum_type_name :"enum" / "intEnum" - enum_shape_members :"{" `ws` 1*(`trait_statements` `identifier` `ws`) "}" + enum_shape_members :"{" `ws` 1*(`trait_statements` `identifier` [`value_assignment`] `ws`) "}" + value_assignment :*`sp` "=" `ws` `node_value` shape_members :"{" `ws` *(`trait_statements` `shape_member` `ws`) "}" - shape_member :`shape_member_kvp` / `shape_member_elided` + shape_member :(`shape_member_kvp` / `shape_member_elided`) [`value_assignment`] shape_member_kvp :`identifier` `ws` ":" `ws` `shape_id` shape_member_elided :"$" `identifier` - list_statement :"list" `ws` `identifier` [`mixins`] `ws` `shape_members` - set_statement :"set" `ws` `identifier` [`mixins`] `ws` `shape_members` - map_statement :"map" `ws` `identifier` [`mixins`] `ws` `shape_members` - structure_statement :"structure" `ws` `identifier` ["for" `shape_id`] [`mixins`] `ws` `shape_members` - union_statement :"union" `ws` `identifier` [`mixins`] `ws` `shape_members` - service_statement :"service" `ws` `identifier` [`mixins`] `ws` `node_object` - operation_statement :"operation" `ws` `identifier` [`mixins`] `ws` `inlineable_properties` + list_statement :"list" `ws` `identifier` [`mixins`] `ws` `shape_members` + set_statement :"set" `ws` `identifier` [`mixins`] `ws` `shape_members` + map_statement :"map" `ws` `identifier` [`mixins`] `ws` `shape_members` + structure_statement :"structure" `ws` `identifier` ["for" `shape_id`] [`mixins`] `ws` `shape_members` + union_statement :"union" `ws` `identifier` [`mixins`] `ws` `shape_members` + service_statement :"service" `ws` `identifier` [`mixins`] `ws` `node_object` + operation_statement :"operation" `ws` `identifier` [`mixins`] `ws` `inlineable_properties` inlineable_properties :"{" *(`inlineable_property` `ws`) `ws` "}" inlineable_property :`node_object_kvp` / `inline_structure` inline_structure :`node_object_key` `ws` ":=" `ws` `inline_structure_value` inline_structure_value :`trait_statements` [`mixins` ws] shape_members - resource_statement :"resource" `ws` `identifier` [`mixins`] `ws` `node_object` + resource_statement :"resource" `ws` `identifier` [`mixins`] `ws` `node_object` .. rubric:: Traits @@ -733,7 +734,9 @@ The following example defines an :ref:`enum` shape: SPADE } -The following defines an :ref:`enum` shape with traits: +Syntactic sugar can be used to assign an :ref:`enumvalue-trait` to an enum +member. The following example defines an enum shape with custom values and +traits: .. code-block:: smithy @@ -742,16 +745,34 @@ The following defines an :ref:`enum` shape with traits: namespace smithy.example enum Suit { - @enumValue("DIAMOND") + @deprecated + DIAMOND = "diamond" + + CLUB = "club" + HEART = "heart" + SPADE = "spade" + } + +The above enum is exactly equivalent to the following enum: + +.. code-block:: smithy + + $version: "2.0" + + namespace smithy.example + + enum Suit { + @deprecated + @enumValue("diamond") DIAMOND - @enumValue("CLUB") + @enumValue("club") CLUB - @enumValue("HEART") + @enumValue("heart") HEART - @enumValue("SPADE") + @enumValue("spade") SPADE } @@ -768,7 +789,23 @@ The :ref:`intEnum` shape is defined using an The :ref:`enumValue trait ` is required for :ref:`intEnum` shapes. -The following example defines an :ref:`intEnum` shape: +Syntactic sugar can be used to assign an :ref:`enumvalue-trait` to an intEnum +member. The following example defines an :ref:`intEnum` shape: + +.. code-block:: smithy + + $version: "2.0" + + namespace smithy.example + + intEnum Suit { + DIAMOND = 1 + CLUB = 2 + HEART = 3 + SPADE = 4 + } + +The above intEnum is exactly equivalent to the following intEnum: .. code-block:: smithy @@ -1113,6 +1150,24 @@ Traits can be applied to structure members: } } +Syntactic sugar can be used to apply the :ref:`default-trait` to a structure +member. The following example: + +.. code-block:: smithy + + structure Example { + normative: Boolean = true + } + +Is exactly equivalent to: + +.. code-block:: smithy + + structure Example { + @default(true) + normative: Boolean + } + .. _idl-union: @@ -1546,8 +1601,6 @@ be checked first. The following example is invalid: } - - .. _documentation-comment: Documentation comment diff --git a/docs/source/1.0/spec/core/model.rst b/docs/source/1.0/spec/core/model.rst index 10de19e110b..0a67efe2568 100644 --- a/docs/source/1.0/spec/core/model.rst +++ b/docs/source/1.0/spec/core/model.rst @@ -397,8 +397,7 @@ The following example defines a shape for each simple type in the FOO } intEnum IntEnum { - @enumValue(1) - FOO + FOO = 1 } .. code-tab:: json @@ -499,17 +498,10 @@ applying the :ref:`enumValue trait `. .. code-block:: smithy enum Suit { - @enumValue("diamond") - DIAMOND - - @enumValue("club") - CLUB - - @enumValue("heart") - HEART - - @enumValue("spade") - SPADE + DIAMOND = "diamond" + CLUB = "club" + HEART = "heart" + SPADE = "spade" } Enums do not support aliasing; all values MUST be unique. @@ -531,9 +523,8 @@ enum members always have a value -------------------------------- If an enum member doesn't have an explicit :ref:`enumValue ` -or :ref:`enumDefault ` trait, an :ref:`enumValue-trait` -will be automatically added to the member where the trait value is the member's -name. +trait, an :ref:`enumValue-trait` is implicitly added to the member with the +trait value set to the member's name. The following model: @@ -551,17 +542,10 @@ Is equivalent to: .. code-block:: smithy enum Suit { - @enumValue("DIAMOND") - DIAMOND - - @enumValue("CLUB") - CLUB - - @enumValue("HEART") - HEART - - @enumValue("SPADE") - SPADE + DIAMOND = "DIAMOND" + CLUB = "CLUB" + HEART = "HEART" + SPADE = "SPADE" } .. _intEnum: @@ -576,20 +560,11 @@ set to a unique integer value. The following example defines an intEnum shape: .. code-block:: smithy intEnum FaceCard { - @enumValue(1) - JACK - - @enumValue(2) - QUEEN - - @enumValue(3) - KING - - @enumValue(4) - ACE - - @enumValue(5) - JOKER + JACK = 1 + QUEEN = 2 + KING = 3 + ACE = 4 + JOKER = 5 } intEnum member names SHOULD NOT contain any lowercase ASCII Latin letters @@ -902,8 +877,9 @@ each member name maps to exactly one :ref:`member ` definition. Structures are defined in the IDL using a :ref:`structure_statement `. -The following example defines a structure with two members, one of which -is marked with the :ref:`required-trait`. +The following example defines a structure with three members, one of which +is marked with the :ref:`required-trait`, and one that is marked with the +:ref:`default-trait` using IDL syntactic sugar. .. tabs:: @@ -916,6 +892,8 @@ is marked with the :ref:`required-trait`. @required baz: Integer + + greeting = "Hello" } .. code-tab:: json @@ -934,6 +912,12 @@ is marked with the :ref:`required-trait`. "traits": { "smithy.api#required": {} } + }, + "greeting": { + "target": "smithy.api#String", + "traits": { + "smithy.api#default": "Hello" + } } } } @@ -946,7 +930,8 @@ is marked with the :ref:`required-trait`. .. rubric:: Adding new members -New members added to existing structures SHOULD be added to the end of the +Members MAY be added to structures. New members MUST NOT be marked with the +:ref:`required-trait`. New members SHOULD be added to the end of the structure. This ensures that programming languages that require a specific data structure layout or alignment for code generated from Smithy models are able to maintain backward compatibility. @@ -958,23 +943,23 @@ by ``$``, followed by the member name. For example, the shape ID of the ``foo`` member in the above example is ``smithy.example#MyStructure$foo``. -.. _structure-nullability: +.. _structure-optionality: -Structure member nullability +Structure member optionality ---------------------------- -Whether a structure member is nullable is based on if the model consumer +Whether a structure member is optional is based on if the model consumer is authoritative (e.g., a server) or non-authoritative (e.g., a client) and determined by evaluating the :ref:`required-trait`, :ref:`default-trait`, :ref:`clientOptional-trait`, and :ref:`input-trait`. -Requiring members -~~~~~~~~~~~~~~~~~ +Required members +~~~~~~~~~~~~~~~~ The :ref:`required-trait` indicates that a value MUST always be present for a -member in order to create a valid structure. Model transformations like code -generators can generate accessors for these members that always return a value. +member in order to create a valid structure. Code generators SHOULD generate +accessors for these members that always return a value. .. code-block:: smithy @@ -985,11 +970,12 @@ generators can generate accessors for these members that always return a value. } -Default zero values -~~~~~~~~~~~~~~~~~~~ +Default values +~~~~~~~~~~~~~~ -A structure member can be given a default zero value using the -:ref:`default-trait`. +A structure member can be given a default value using the :ref:`default-trait`. +The following example uses syntactic sugar in the Smithy IDL allows to assign +a default value. .. code-block:: smithy @@ -997,55 +983,41 @@ A structure member can be given a default zero value using the @required years: Integer - // Defaults to 0 - @default - days: Integer + days: Integer = 0 } -.. important:: - - An explicitly provided default zero value and a member that defaults to - the zero value are indistinguishable. - -.. seealso:: - - :ref:`default-values` for a list of shapes and their default zero values - Evolving requirements and members ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Business requirements change; what is required today might not be required -tomorrow. Smithy provides several ways to make it so that required members no -longer need to be provided without breaking previously generated code. +Requirements change; what is required today might not be required tomorrow. +Smithy provides several ways to make it so that required members no longer +need to be provided without breaking previously generated code. .. rubric:: Migrating ``@required`` to ``@default`` -If a ``required`` member can no longer be required due to a change in business -requirements, the ``required`` trait MAY be removed and replaced with the -:ref:`default-trait`. The member is still considered always present to tools -like code generators, but instead of requiring the value to be provided by an -end-user, a default zero value is automatically provided if missing. For -example, the previous ``TimeSpan`` model can be backward compatibly changed to: +If a ``required`` member no longer needs to be be required, the ``required`` +trait MAY be removed and replaced with the :ref:`default-trait`. The member +is still considered always present to tools like code generators, but instead +of requiring the value to be provided by an end-user, a default value is +automatically provided if missing. For example, the previous ``TimeSpan`` +model can be backward compatibly changed to: .. code-block:: smithy structure TimeSpan { // @required is replaced with @default - @default - years: Integer - - @default - days: Integer + years: Integer = 0 + days: Integer = 0 } -.. rubric:: Requiring members to be nullable +.. rubric:: Requiring members to be optional -The :ref:`clientOptional-trait` is used to indicate that a member that is currently -required by authoritative model consumers like servers MAY become completely -optional in the future. Non-authoritative model consumers like client code -generators MUST treat the member as if it is not required and has no default -zero value. +The :ref:`clientOptional-trait` is used to indicate that a member that is +currently required by authoritative model consumers like servers MAY become +completely optional in the future. Non-authoritative model consumers like +client code generators MUST treat the member as if it is not required and +has no default value. For example, the following structure: @@ -1065,16 +1037,17 @@ Can be backward-compatibly updated to remove the ``required`` trait: summary: String } -Replacing the ``required`` and ``clientOptional`` trait with the ``default`` trait -is *not* a backward compatible change because model consumers would transition -from assuming the value is nullable to assuming that it is always present due -to a default zero value. +Replacing the ``required`` and ``clientOptional`` trait with the ``default`` +trait is *not* a backward compatible change because model consumers would +transition from assuming the value is nullable to assuming that it is always +present due to a default value. .. note:: - Authoritative model consumers MAY choose to ignore the ``clientOptional`` trait. + Authoritative model consumers MAY choose to ignore the ``clientOptional`` + trait. -.. rubric:: Using the ``@input`` trait for more model evolution options +.. rubric:: Model evolution and the ``@input`` trait The :ref:`input-trait` specializes a structure as the input of a single operation. Transitioning top-level members from ``required`` to optional is diff --git a/docs/source/1.0/spec/core/prelude-model.rst b/docs/source/1.0/spec/core/prelude-model.rst index a7db1af4dfd..8151652d03b 100644 --- a/docs/source/1.0/spec/core/prelude-model.rst +++ b/docs/source/1.0/spec/core/prelude-model.rst @@ -39,41 +39,20 @@ Prelude shapes document Document - @box boolean Boolean - boolean PrimitiveBoolean - - @box byte Byte - byte PrimitiveByte - - @box short Short - short PrimitiveShort - - @box integer Integer - integer PrimitiveInteger - - @box long Long - long PrimitiveLong - - @box float Float - float PrimitiveFloat - - @box double Double - double PrimitiveDouble - /// The single unit type shape, similar to Void and None in other /// languages, used to represent no meaningful value. @unitType diff --git a/docs/source/1.0/spec/core/traits.rst b/docs/source/1.0/spec/core/traits.rst index 610ca7f6f04..0d929ba53a7 100644 --- a/docs/source/1.0/spec/core/traits.rst +++ b/docs/source/1.0/spec/core/traits.rst @@ -75,74 +75,52 @@ Value type :ref:`Defining traits `. -.. smithy-trait:: smithy.api#enumDefault -.. _enumDefault-trait: +.. smithy-trait:: smithy.api#enumValue +.. _enumValue-trait: ---------------------- -``enumDefault`` trait ---------------------- +------------------- +``enumValue`` trait +------------------- Summary - Indicates that the targeted :ref:`enum` or :ref:`intEnum` member represents - the default value. + Defines the value of an :ref:`enum ` or :ref:`intEnum `. + For enum shapes, a non-empty string value must be used. For intEnum + shapes, an integer value must be used. Trait selector ``:is(enum, intEnum) > member`` Value type - Annotation trait. - -The ``enumDefault`` trait can be applied to a member of an :ref:`enum` -or :ref:`intEnum` to indicate that that member should be used for the -default value of the enum. - -.. seealso:: - the :ref:`default-values` for the value of the default enum. + ``string`` or ``integer`` -.. code-block:: +.. code-block:: smithy $version: "2.0" namespace smithy.example enum Enum { - @enumDefault - DEFAULT - - NON_DEFAULT + @enumValue("foo") + FOO } + intEnum IntEnum { + @enumValue(1) + FOO + } -.. smithy-trait:: smithy.api#enumValue -.. _enumValue-trait: - -------------------- -``enumValue`` trait -------------------- - -Summary - Defines the value of the targeted :ref:`enum` or :ref:`intEnum` member. -Trait selector - ``:is(enum, intEnum) > member`` -Value type - `string` or `integer` - -Defines the value of an :ref:`enum` or :ref:`intEnum`. For -:ref:`enum shapes `, a string value must be used. For -:ref:`intEnum shapes `, an integer value must be used. +The following enum definition uses syntactic sugar that is exactly equivalent: -.. code-block:: +.. code-block:: smithy $version: "2.0" namespace smithy.example enum Enum { - @enumValue("foo") - FOO + FOO = "foo" } intEnum IntEnum { - @enumValue(1) - FOO + FOO = 1 } diff --git a/docs/source/1.0/spec/core/traits/auth-traits.rst.template b/docs/source/1.0/spec/core/traits/auth-traits.rst.template index d083072047a..bf34336c9d3 100644 --- a/docs/source/1.0/spec/core/traits/auth-traits.rst.template +++ b/docs/source/1.0/spec/core/traits/auth-traits.rst.template @@ -104,8 +104,9 @@ can also support configuration settings. } @private - @enum([{value: "SHA-2"}]) - string AlgorithmAuthAlgorithm + enum AlgorithmAuthAlgorithm { + SHA2: "SHA-2" + } @algorithmAuth(algorithm: "SHA-2") service WeatherService { diff --git a/docs/source/1.0/spec/core/traits/constraint-traits.rst.template b/docs/source/1.0/spec/core/traits/constraint-traits.rst.template index 49946d8a0bd..14a1e06107d 100644 --- a/docs/source/1.0/spec/core/traits/constraint-traits.rst.template +++ b/docs/source/1.0/spec/core/traits/constraint-traits.rst.template @@ -466,15 +466,13 @@ of the targeted numeric shape to which it is applied. Summary Marks a structure member as required, meaning a value for the member MUST - be present and not set to ``null``. + be provided. Trait selector ``structure > member`` *Member of a structure* Value type Annotation trait. -Conflicts with - :ref:`default-trait` .. important:: The required trait isn't just for inputs @@ -491,7 +489,7 @@ Conflicts with .. seealso:: - * :ref:`structure-nullability` + * :ref:`structure-optionality` * :ref:`default-trait` * :ref:`clientOptional-trait` * :ref:`input-trait` diff --git a/docs/source/1.0/spec/core/traits/type-refinement-traits.rst.template b/docs/source/1.0/spec/core/traits/type-refinement-traits.rst.template index 57998f632a2..664cfdef959 100644 --- a/docs/source/1.0/spec/core/traits/type-refinement-traits.rst.template +++ b/docs/source/1.0/spec/core/traits/type-refinement-traits.rst.template @@ -19,131 +19,148 @@ the type of a shape. ================= Summary - Provides a structure member with a default zero value. + Provides a structure member with a default value. Trait selector - ``structure > member :test(> :is(simpleType, collection, map))`` + ``structure > member :test(> :is(simpleType, list, map))`` - A member of a structure that targets a simple type, list, set, or map. + A member of a structure that targets a simple type, list, or map. Value type - Annotation trait. -Conflicts with + Document type. +See also + * :ref:`structure-optionality` * :ref:`required-trait` + * :ref:`clientOptional-trait` + * :ref:`input-trait` -The default trait can be applied to structure members to indicate that the targeted -shape has a default, zero value. +The ``@default`` trait assigns a default value to a structure member using +a document type. The following example defines a structure with a "language" +member that has a default value: -The following example defines a structure with a ``@default`` "title" member that -has a default zero value: +.. code-block:: smithy + + structure Message { + @required + title: String + + language: Language = "en" + } + + enum Language { + EN = "en" + } + +The above example uses syntactic sugar to apply the ``@default`` trait. The +``Message`` definition is exactly equivalent to: .. code-block:: smithy structure Message { - @default - title: String // defaults to "" + @required + title: String + + @default("en") + language: Language } -.. seealso:: - * :ref:`structure-nullability` - * :ref:`required-trait` - * :ref:`clientOptional-trait` - * :ref:`input-trait` +Default value constraints +------------------------- +The value of the ``@default`` trait MUST be compatible with the shape targeted +by the member and any applied constraint traits (for example, values for +numeric types MUST be numbers that fit within the targeted type and match any +:ref:`range ` constraints, string types match any +:ref:`length ` or :ref:`pattern ` traits, etc). -.. _default-values: +The following shapes have restrictions on their default values: -Default zero values -------------------- +* enum: can be set to any valid string *value* of the enum. +* intEnum: can be set to any valid integer *value* of the enum. +* document: can be set to ``true``, ``false``, string, empty list, or empty + map. +* list: can only be set to an empty list. +* map: can only be set to an empty map. +* structure: no default value. +* union: no default value. -The following table describes the default zero value of each kind of shape. -Programming languages and code generators that cannot initialize structure -members with the following default values SHOULD represent those members as -nullable as this is semantically equivalent to the default zero value. -.. list-table:: - :header-rows: 1 - :widths: 10 10 80 +Impact on API design +-------------------- + +The ``@default`` trait SHOULD NOT be used for partial updates or patch style +operations where it is critical to differentiate between omitted values and +explicitly set values. Assigning default values is typically something that +occurs during deserialization, and as such, it is impossible for a server to +differentiate between whether a property was set to its default value or if a +property was omitted. + + +Updating default values +----------------------- + +The default value of a member SHOULD NOT be changed. However, it MAY be +necessary in extreme cases to change a default value if changing the default +value addresses a customer-impacting issue or availability issue for a service. +Changing default values can result in parties disagreeing on the default value +of a member because they are using different versions of the same model. + + +Default value serialization and deserialization +----------------------------------------------- + +When deserializing a structure, a reader MUST set the member to its default +value if the member is missing. Outside of deserialization, there MUST NOT be +any discernible difference between an explicitly serialized member or a member +that was assigned the default value. If such a distinction is needed, then the +``@default`` trait is inappropriate. + +Authoritative model consumers like servers SHOULD always serialize default +values to remove any ambiguity about the value of the most up to default +value. When persisting data structures, regardless of if they are persisted by +a client or server, default values SHOULD be persisted to ensure that model +changes do not impact previously serialized values. - * - Shape Type - - Zero Value - - Description - * - boolean - - ``false`` - - Boolean false. - * - numbers - - ``0`` - - Numeric zero. - * - intEnum - - ``0`` - - Numeric zero. - * - string - - empty string - - - * - enum - - empty string - - - * - blob - - empty blob - - This includes blob shapes marked with the :ref:`streaming-trait`. - * - timestamp - - Unix epoch - - Zero seconds since the epoch (for example, ``0`` or - ``1970-01-01T00:00:00Z``). - * - document - - ``null`` - - A null document value. - * - list - - empty list - - - * - set - - empty set - - - * - map - - empty map - - - * - structure - - N/A - - Structures have no default value. - * - union - - N/A - - Unions have no default value. A union MUST be set to one of its - variants for it to be valid, and unions have no default variant. - - -Constraint validation ---------------------- - -Constraint traits are not evaluated on structure members marked with the -default trait when the value of the member is the default value. - - -Guidance on protocol design ---------------------------- - -Protocols MAY choose if and how the default trait impacts serialization and -deserialization. However, protocol designers should consider the following -best-practices: - -1. Serializing the default zero value of a member marked with the default - trait can lead to unintended information disclosure. For example, consider - a newly introduced structure member marked with the default trait that is - only exposed to customers of a service that are allowlisted into a private - beta. Serializing the zero values of these members could expose the feature - to customers that are not part of the private beta because they would see - the member serialized in messages they receive from the service. -2. Protocol deserialization implementations SHOULD tolerate receiving a - serialized default zero value. This also accounts for older clients that - think a structure member is required, but the service has since transitioned - the member to use the default trait. -3. Client implementations SHOULD tolerate structure members marked as - :ref:`required ` that have no serialized value. For example, - if a service migrates a member from required to default, then older clients - SHOULD gracefully handle the zero value of the member being omitted on the - wire. In this case, rather than failing, a client SHOULD set the member - value to its default zero value. Failing to deserialize the structure is a - bad outcome because what the service perceived as a backward compatible - change (i.e., replacing the :ref:`required-trait` with the default trait) - could break previously generated clients. + +``@default`` and ``@required`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A member that is marked as both ``@required`` and ``@default`` indicates that +the member MUST be serialized even if the member is set to the default value. + +.. code-block:: smithy + + structure Message { + // this member must be serialized + @required + title: String = "Hello" + } + + +Default values and clients +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To allow default values to be controlled by servers, clients SHOULD NOT +serialize default values unless the member is marked as ``@required`` or if +the value of the member is explicitly set by the client to the default value. +This implies that clients need to implement "presence tracking" of defaulted +members. + +If a mis-configured server fails to serialize a value for a required member, +to avoid downtime, clients MAY attempt to fill in an appropriate default value +for the member: + +* ``@default``: If the member has the ``@default`` trait, use its value +* boolean: false +* numbers: 0 +* timestamp: 0 seconds since the Unix epoch +* string and blob: "" +* document: null +* list: [] +* map: {} +* enum, intEnum, union: The unknown variant. These types SHOULD define an + unknown variant to account for receiving unknown members. +* structure: {} if possible, otherwise a deserialization error. +* union: a deserialization error. .. smithy-trait:: smithy.api#clientOptional @@ -154,26 +171,28 @@ best-practices: Summary Requires that non-authoritative generators like clients treat a structure - member as nullable regardless of if the member is also marked with the - :ref:`required-trait`. + member as optional regardless of if the member is also marked with the + :ref:`required-trait` or :ref:`default-trait`. Trait selector - structure > member + ``structure > member`` Value type Annotation trait +See also + * :ref:`structure-optionality` + * :ref:`required-trait` + * :ref:`default-trait` + * :ref:`input-trait` -What is required today might not be required tomorrow. For cases when a service -isn't sure if a member will be required forever, they can mark a ``@required`` -member as ``@clientOptional`` to ensure that non-authoritative consumers of the -model treat the member as optional (for example, clients MUST treat such members -as optional while servers could treat them as always present because they have -perfect knowledge of their own model). The ``@required`` trait can be backward -compatibly removed from a member marked as ``@clientOptional`` (and does not -need to be replaced with the ``@default`` trait). This causes the ``@required`` -trait to function as server-side validation rather than something that changes -generated code. +For cases when a service is unsure if a member will be required forever, the +member can be marked with the ``@clientOptional`` trait to ensure that +non-authoritative consumers of the model like clients treat the member as +optional. The ``@required`` trait can be backward compatibly removed from a +member marked as ``@clientOptional`` (and does not need to be replaced with +the ``@default`` trait). This causes the ``@required`` and ``@default`` traits +to function only as a server-side concern. -For example, the ``@required`` trait on ``foo`` in the following structure is -considered a validation constraint rather than a type refinement trait: +The ``@required`` trait on ``foo`` in the following structure is considered a +validation constraint rather than a type refinement trait: .. code-block:: smithy @@ -185,7 +204,7 @@ considered a validation constraint rather than a type refinement trait: .. note:: - Structure members in Smithy are automatically considered nullable. For example, + Structure members in Smithy are automatically considered optional. For example, the following structure: .. code-block:: smithy @@ -203,13 +222,6 @@ considered a validation constraint rather than a type refinement trait: baz: String } -.. seealso:: - - * :ref:`structure-nullability` - * :ref:`required-trait` - * :ref:`default-trait` - * :ref:`input-trait` - .. smithy-trait:: smithy.api#error .. _error-trait: @@ -298,6 +310,20 @@ Value type Conflicts with * :ref:`output-trait` * :ref:`error-trait` +See also + * :ref:`structure-optionality` + +The following example defines an ``@input`` structure: + +.. code-block:: smithy + + @input + structure SomeOperationInput { + @required + name: String + } + +.. rubric:: ``@input`` structure constraints Structure shapes marked with the ``@input`` trait MUST adhere to the following constraints: @@ -323,8 +349,6 @@ as well). This gives service teams the ability to remove the ``@required`` trait from top-level input members and loosen requirements without risking breaking previously generated clients. -.. seealso:: :ref:`structure-nullability` - .. smithy-trait:: smithy.api#output .. _output-trait: diff --git a/docs/source/implementations.rst b/docs/source/implementations.rst index deb08cff7c2..b5116c6fb48 100644 --- a/docs/source/implementations.rst +++ b/docs/source/implementations.rst @@ -126,9 +126,9 @@ Build tooling - The Smithy SBT plugin transforms Smithy specifications into protocol-agnostic Scala clients and servers. ---------------- +---------------------- Client code generators ---------------- +---------------------- The following code generators are in early development. There's no guarantee of polish or that they work for all use cases. diff --git a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddDefaultConfigSettings.java b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddDefaultConfigSettings.java index 488e17a78a3..4e507f846d3 100644 --- a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddDefaultConfigSettings.java +++ b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddDefaultConfigSettings.java @@ -20,7 +20,10 @@ import software.amazon.smithy.openapi.OpenApiConfig; /** - * API Gateway does not allow characters like "_". + * Disables OpenAPI and JSON Schema features not supported by API Gateway. + * + *

API Gateway does not allow characters like "_". API Gateway + * doesn't support the "default" trait. */ final class AddDefaultConfigSettings implements ApiGatewayMapper { @Override @@ -31,5 +34,6 @@ public List getApiTypes() { @Override public void updateDefaultSettings(Model model, OpenApiConfig config) { config.setAlphanumericOnlyRefs(true); + config.getDisableFeatures().add("default"); } } diff --git a/smithy-aws-apigateway-openapi/src/test/java/software/amazon/smithy/aws/apigateway/openapi/AddDefaultConfigSettingsTest.java b/smithy-aws-apigateway-openapi/src/test/java/software/amazon/smithy/aws/apigateway/openapi/AddDefaultConfigSettingsTest.java index 1ee397fbab2..a026afd5ac8 100644 --- a/smithy-aws-apigateway-openapi/src/test/java/software/amazon/smithy/aws/apigateway/openapi/AddDefaultConfigSettingsTest.java +++ b/smithy-aws-apigateway-openapi/src/test/java/software/amazon/smithy/aws/apigateway/openapi/AddDefaultConfigSettingsTest.java @@ -6,11 +6,13 @@ import java.util.Optional; import org.junit.jupiter.api.Test; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.NodePointer; import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.openapi.OpenApiConfig; import software.amazon.smithy.openapi.fromsmithy.OpenApiConverter; +import software.amazon.smithy.utils.IoUtils; public class AddDefaultConfigSettingsTest { @Test @@ -31,4 +33,21 @@ public void addsDefaultConfigSettings() { NodePointer pointer = NodePointer.parse("/components/schemas/FooBaz"); assertThat(pointer.getValue(result), not(Optional.empty())); } + + @Test + public void omitsDefaultTraits() { + Model model = Model.assembler() + .discoverModels(getClass().getClassLoader()) + .addImport(getClass().getResource("omits-default-trait.smithy")) + .assemble() + .unwrap(); + + OpenApiConfig config = new OpenApiConfig(); + config.setService(ShapeId.from("example.smithy#MyService")); + ObjectNode result = OpenApiConverter.create() + .config(config) + .convertToNode(model); + + Node.assertEquals(result, Node.parse(IoUtils.readUtf8Resource(getClass(), "omits-default-trait.openapi.json"))); + } } diff --git a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/omits-default-trait.openapi.json b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/omits-default-trait.openapi.json new file mode 100644 index 00000000000..198eb7c9a7a --- /dev/null +++ b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/omits-default-trait.openapi.json @@ -0,0 +1,67 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "MyService", + "version": "2020-07-02" + }, + "paths": { + "/defaults": { + "post": { + "operationId": "HasDefault", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HasDefaultRequestContent" + } + } + } + }, + "responses": { + "200": { + "description": "HasDefault 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HasDefaultResponseContent" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "HasDefaultRequestContent": { + "type": "object", + "properties": { + "foo": { + "type": "string" + }, + "bar": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "HasDefaultResponseContent": { + "type": "object", + "properties": { + "foo": { + "type": "string" + }, + "bar": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } +} diff --git a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/omits-default-trait.smithy b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/omits-default-trait.smithy new file mode 100644 index 00000000000..92eb6e59fe7 --- /dev/null +++ b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/omits-default-trait.smithy @@ -0,0 +1,28 @@ +$version: "2.0" + +namespace example.smithy + +use aws.protocols#httpChecksum +use aws.protocols#restJson1 + +@restJson1 +service MyService { + version: "2020-07-02", + operations: [HasDefault] +} + +@http(method: "POST", uri: "/defaults") +operation HasDefault { + input := { + foo: String = "" + bar: StringList = [] + } + output := { + foo: String = "" + bar: StringList = [] + } +} + +list StringList { + member: String +} diff --git a/smithy-aws-apigateway-traits/src/main/resources/META-INF/smithy/aws.apigateway.smithy b/smithy-aws-apigateway-traits/src/main/resources/META-INF/smithy/aws.apigateway.smithy index 4b4ca5c27ec..1a4dfdeb28e 100644 --- a/smithy-aws-apigateway-traits/src/main/resources/META-INF/smithy/aws.apigateway.smithy +++ b/smithy-aws-apigateway-traits/src/main/resources/META-INF/smithy/aws.apigateway.smithy @@ -78,7 +78,7 @@ structure integration { /// valid value is `INTERNET` for connections through the public routable /// internet or `VPC_LINK` for private connections between API Gateway and /// a network load balancer in a VPC. The default value is `INTERNET`. - connectionType: ConnectionType + connectionType: ConnectionType = "INTERNET" /// An API-specific tag group of related cached parameters. cacheNamespace: String @@ -301,20 +301,16 @@ enum IntegrationType { /// An integration with AWS Lambda functions or other AWS services such as /// Amazon DynamoDB, Amazon Simple Notification Service or Amazon Simple /// Queue Service. - @enumValue("aws") - AWS + AWS = "aws" /// An integration with AWS Lambda functions. - @enumValue("aws_proxy") - AWS_PROXY + AWS_PROXY = "aws_proxy" /// An integration with an HTTP backend. - @enumValue("http") - HTTP + HTTP = "http" /// An integration with an HTTP backend. - @enumValue("http_proxy") - HTTP_PROXY + HTTP_PROXY = "http_proxy" } /// Defines the passThroughBehavior for the integration @@ -325,21 +321,18 @@ enum PassThroughBehavior { /// the integration request. If a template is defined when this option is /// selected, the method request of an unmapped content-type will be /// rejected with an HTTP 415 Unsupported Media Type response. - @enumValue("when_no_templates") - WHEN_NO_TEMPLATES + WHEN_NO_TEMPLATES = "when_no_templates" /// Passes the method request body through the integration request to the /// back end without transformation when the method request content type /// does not match any content type associated with the mapping templates /// defined in the integration request. - @enumValue("when_no_match") - WHEN_NO_MATCH + WHEN_NO_MATCH = "when_no_match" /// Rejects the method request with an HTTP 415 Unsupported Media Type /// response when either the method request content type does not match any /// content type associated with the mapping templates defined in the /// integration request or no mapping template is defined in the integration /// request. - @enumValue("never") - NEVER + NEVER = "never" } diff --git a/smithy-aws-cloudformation-traits/src/main/resources/META-INF/smithy/aws.cloudformation.smithy b/smithy-aws-cloudformation-traits/src/main/resources/META-INF/smithy/aws.cloudformation.smithy index edd1f9cc40f..de07152b255 100644 --- a/smithy-aws-cloudformation-traits/src/main/resources/META-INF/smithy/aws.cloudformation.smithy +++ b/smithy-aws-cloudformation-traits/src/main/resources/META-INF/smithy/aws.cloudformation.smithy @@ -46,36 +46,31 @@ enum cfnMutability { /// member does not have any mutability restrictions, meaning that it /// can be specified by the user and returned in a `read` or `list` /// request. - @enumValue("full") - FULL + FULL = "full" /// Indicates that the CloudFormation property generated from this /// member can be specified only during resource creation and can be /// returned in a `read` or `list` request. - @enumValue("create-and-read") - CREATE_AND_READ + CREATE_AND_READ = "create-and-read" /// Indicates that the CloudFormation property generated from this /// member can be specified only during resource creation and cannot /// be returned in a `read` or `list` request. MUST NOT be set if the /// member is also marked with the `@additionalIdentifier` trait. - @enumValue("create") - CREATE + CREATE = "create" /// Indicates that the CloudFormation property generated from this /// member can be returned by a `read` or `list` request, but /// cannot be set by the user. - @enumValue("read") - READ + READ = "read" /// Indicates that the CloudFormation property generated from this /// member can be specified by the user, but cannot be returned by a /// `read` or `list` request. MUST NOT be set if the member is also /// marked with the `@additionalIdentifier` trait. - @enumValue("write") - WRITE + WRITE = "write" } /// Indicates that a Smithy resource is a CloudFormation resource. diff --git a/smithy-aws-protocol-tests/model/aws-config.smithy b/smithy-aws-protocol-tests/model/aws-config.smithy index ff3a9f9d703..321956d6246 100644 --- a/smithy-aws-protocol-tests/model/aws-config.smithy +++ b/smithy-aws-protocol-tests/model/aws-config.smithy @@ -98,24 +98,14 @@ structure RetryConfig { /// Controls the S3 addressing bucket style. enum S3AddressingStyle { - @enumValue("auto") - AUTO - - @enumValue("path") - PATH - - @enumValue("virtual") - VIRTUAL + AUTO = "auto" + PATH = "path" + VIRTUAL = "virtual" } /// Controls the strategy used for retries. enum RetryMode { - @enumValue("legacy") - LEGACY - - @enumValue("standard") - STANDARD - - @enumValue("adaptive") - ADAPTIVE + LEGACY = "legacy" + STANDARD = "standard" + ADAPTIVE = "adaptive" } diff --git a/smithy-aws-protocol-tests/model/awsJson1_1/services/machinelearning.smithy b/smithy-aws-protocol-tests/model/awsJson1_1/services/machinelearning.smithy index 049b054b19a..e16cc9fbcef 100644 --- a/smithy-aws-protocol-tests/model/awsJson1_1/services/machinelearning.smithy +++ b/smithy-aws-protocol-tests/model/awsJson1_1/services/machinelearning.smithy @@ -136,11 +136,8 @@ map ScoreValuePerLabelMap { } enum DetailsAttributes { - @enumValue("PredictiveModelType") - PREDICTIVE_MODEL_TYPE - - @enumValue("Algorithm") - ALGORITHM + PREDICTIVE_MODEL_TYPE = "PredictiveModelType" + ALGORITHM = "Algorithm" } @length( diff --git a/smithy-aws-protocol-tests/model/restJson1/http-string-payload.smithy b/smithy-aws-protocol-tests/model/restJson1/http-string-payload.smithy index 9f362d0960d..1e6612f458b 100644 --- a/smithy-aws-protocol-tests/model/restJson1/http-string-payload.smithy +++ b/smithy-aws-protocol-tests/model/restJson1/http-string-payload.smithy @@ -36,8 +36,7 @@ structure EnumPayloadInput { } enum StringEnum { - @enumValue("enumvalue") - V + V = "enumvalue" } @http(uri: "/StringPayload", method: "POST") diff --git a/smithy-aws-protocol-tests/model/restJson1/services/glacier.smithy b/smithy-aws-protocol-tests/model/restJson1/services/glacier.smithy index 3e380df8786..67659bdfeae 100644 --- a/smithy-aws-protocol-tests/model/restJson1/services/glacier.smithy +++ b/smithy-aws-protocol-tests/model/restJson1/services/glacier.smithy @@ -206,9 +206,8 @@ structure UploadArchiveInput { archiveDescription: string, @httpHeader("x-amz-sha256-tree-hash") checksum: string, - @default @httpPayload - body: Stream, + body: Stream = "", } structure UploadMultipartPartInput { @@ -225,9 +224,8 @@ structure UploadMultipartPartInput { checksum: string, @httpHeader("Content-Range") range: string, - @default @httpPayload - body: Stream, + body: Stream = "", } structure UploadMultipartPartOutput { diff --git a/smithy-aws-protocol-tests/model/restJson1/streaming.smithy b/smithy-aws-protocol-tests/model/restJson1/streaming.smithy index 11b3e70579e..6ef23b46d26 100644 --- a/smithy-aws-protocol-tests/model/restJson1/streaming.smithy +++ b/smithy-aws-protocol-tests/model/restJson1/streaming.smithy @@ -91,9 +91,8 @@ structure StreamingTraitsInputOutput { @httpHeader("X-Foo") foo: String, - @default @httpPayload - blob: StreamingBlob, + blob: StreamingBlob = "", } @streaming @@ -152,9 +151,8 @@ structure StreamingTraitsRequireLengthInput { @httpHeader("X-Foo") foo: String, - @default @httpPayload - blob: FiniteStreamingBlob, + blob: FiniteStreamingBlob = "", } @streaming @@ -214,9 +212,8 @@ structure StreamingTraitsWithMediaTypeInputOutput { @httpHeader("X-Foo") foo: String, - @default @httpPayload - blob: StreamingTextPlainBlob, + blob: StreamingTextPlainBlob = "" } @streaming diff --git a/smithy-aws-protocol-tests/model/restJson1/validation/malformed-enum.smithy b/smithy-aws-protocol-tests/model/restJson1/validation/malformed-enum.smithy index 1bb391b4a20..85666e3d5ce 100644 --- a/smithy-aws-protocol-tests/model/restJson1/validation/malformed-enum.smithy +++ b/smithy-aws-protocol-tests/model/restJson1/validation/malformed-enum.smithy @@ -192,11 +192,8 @@ structure MalformedEnumInput { } enum EnumString { - @enumValue("abc") - ABC - - @enumValue("def") - DEF + ABC = "abc" + DEF = "def" } list EnumList { diff --git a/smithy-aws-protocol-tests/model/restJson1/validation/recursive-structures.smithy b/smithy-aws-protocol-tests/model/restJson1/validation/recursive-structures.smithy index 9c40305e03b..2467193026b 100644 --- a/smithy-aws-protocol-tests/model/restJson1/validation/recursive-structures.smithy +++ b/smithy-aws-protocol-tests/model/restJson1/validation/recursive-structures.smithy @@ -86,11 +86,8 @@ structure RecursiveStructuresInput { } enum RecursiveEnumString { - @enumValue("abc") - ABC - - @enumValue("def") - DEF + ABC = "abc" + DEF = "def" } union RecursiveUnionOne { diff --git a/smithy-aws-protocol-tests/model/restXml/services/s3.smithy b/smithy-aws-protocol-tests/model/restXml/services/s3.smithy index b0faf8efb2c..bb5f151ef43 100644 --- a/smithy-aws-protocol-tests/model/restXml/services/s3.smithy +++ b/smithy-aws-protocol-tests/model/restXml/services/s3.smithy @@ -460,6 +460,5 @@ string Token enum BucketLocationConstraint { @suppress(["EnumShape"]) - @enumValue("us-west-2") - us_west_2 + us_west_2 = "us-west-2" } diff --git a/smithy-aws-protocol-tests/model/shared-types.smithy b/smithy-aws-protocol-tests/model/shared-types.smithy index 79fc00bad16..cf1356cd83c 100644 --- a/smithy-aws-protocol-tests/model/shared-types.smithy +++ b/smithy-aws-protocol-tests/model/shared-types.smithy @@ -77,20 +77,11 @@ list TimestampList { } enum FooEnum { - @enumValue("Foo") - FOO - - @enumValue("Baz") - BAZ - - @enumValue("Bar") - BAR - - @enumValue("1") - ONE - - @enumValue("0") - ZERO + FOO = "Foo" + BAZ = "Baz" + BAR = "Bar" + ONE = "1" + ZERO = "0" } list FooEnumList { diff --git a/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.api.smithy b/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.api.smithy index e6e181f0270..8aff1304baf 100644 --- a/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.api.smithy +++ b/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.api.smithy @@ -126,30 +126,25 @@ enum data { /// connection with the customer’s accounts and any computational results /// that customers or any customer end user derive from the foregoing /// through their use of AWS services. - @enumValue("content") - CUSTOMER_CONTENT + CUSTOMER_CONTENT = "content" /// Account information means information about customers that customers /// provide to AWS in connection with the creation or administration of /// customers’ accounts. - @enumValue("account") - CUSTOMER_ACCOUNT_INFORMATION + CUSTOMER_ACCOUNT_INFORMATION = "account" /// Service Attributes means service usage data related to a customer’s /// account, such as resource identifiers, metadata tags, security and /// access roles, rules, usage policies, permissions, usage statistics, /// logging data, and analytics. - @enumValue("usage") - SERVICE_ATTRIBUTES + SERVICE_ATTRIBUTES = "usage" /// Designates metadata tags applied to AWS resources. - @enumValue("tagging") - TAG_DATA + TAG_DATA = "tagging" /// Designates security and access roles, rules, usage policies, and /// permissions. - @enumValue("permissions") - PERMISSIONS_DATA + PERMISSIONS_DATA = "permissions" } /// Defines a service, resource, or operation as operating on the data plane. diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/Upgrade1to2Command.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/Upgrade1to2Command.java index 81250911cc4..558cf5bfb99 100644 --- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/Upgrade1to2Command.java +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/Upgrade1to2Command.java @@ -49,12 +49,12 @@ import software.amazon.smithy.model.loader.ParserUtils; import software.amazon.smithy.model.loader.Prelude; import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.NumberShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeType; import software.amazon.smithy.model.shapes.ShapeVisitor; import software.amazon.smithy.model.shapes.SmithyIdlModelSerializer; import software.amazon.smithy.model.shapes.StringShape; -import software.amazon.smithy.model.traits.AnnotationTrait; import software.amazon.smithy.model.traits.BoxTrait; import software.amazon.smithy.model.traits.DefaultTrait; import software.amazon.smithy.model.traits.EnumTrait; @@ -289,7 +289,18 @@ private void handleMemberShape(MemberShape shape) { if (memberLocation.getColumn() > 1) { padding = StringUtils.repeat(' ', memberLocation.getColumn() - 1); } - writer.insertLine(shape.getSourceLocation().getLine(), padding + "@default"); + Shape target = completeModel.expectShape(shape.getTarget()); + String defaultValue = ""; + if (target.isBooleanShape()) { + defaultValue = "false"; + } else if (target instanceof NumberShape) { + defaultValue = "0"; + } else if (target.isBlobShape() || target.isStringShape()) { + defaultValue = "\"\""; + } else { + throw new UnsupportedOperationException("Unexpected default: " + target); + } + writer.insertLine(shape.getSourceLocation().getLine(), padding + "@default(" + defaultValue + ")"); } if (shape.hasTrait(BoxTrait.class)) { @@ -322,7 +333,7 @@ private void replacePrimitiveTarget(MemberShape member) { private boolean hasSyntheticDefault(MemberShape shape) { Optional defaultLocation = shape.getTrait(DefaultTrait.class) - .map(AnnotationTrait::getSourceLocation); + .map(Trait::getSourceLocation); // When Smithy injects the default trait, it sets the source // location equal to the shape's source location. This is // impossible in any other scenario, so we can use this info diff --git a/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/Upgrade1to2CommandTest.java b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/Upgrade1to2CommandTest.java index 4304d6e9efb..787f26791c5 100644 --- a/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/Upgrade1to2CommandTest.java +++ b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/Upgrade1to2CommandTest.java @@ -110,13 +110,13 @@ public void testUpgradeDirectoryWithJar() throws Exception { } - private void assertDirEqual(Path actualDir, Path excpectedDir) throws Exception { + private void assertDirEqual(Path actualDir, Path expectedDir) throws Exception { Set files = Files.walk(actualDir) .filter(Files::isRegularFile) .filter(path -> path.toString().endsWith(".smithy")) .collect(Collectors.toSet()); for (Path actual : files) { - Path expected = excpectedDir.resolve(actualDir.relativize(actual)); + Path expected = expectedDir.resolve(actualDir.relativize(actual)); assertThat(IoUtils.readUtf8File(actual), equalTo(IoUtils.readUtf8File(expected))); } } diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/enum-with-traits.v2.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/enum-with-traits.v2.smithy index f702d63af4c..e4ea419331c 100644 --- a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/enum-with-traits.v2.smithy +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/enum-with-traits.v2.smithy @@ -4,24 +4,18 @@ namespace com.example @internal enum TraitAfterEnum { - @enumValue("foo") - FOO - @enumValue("bar") - BAR + FOO = "foo" + BAR = "bar" } @internal enum TraitBeforeEnum { - @enumValue("foo") - FOO - @enumValue("bar") - BAR + FOO = "foo" + BAR = "bar" } @internal() enum AnnotationTraitWithParens { - @enumValue("foo") - FOO - @enumValue("bar") - BAR + FOO = "foo" + BAR = "bar" } diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/enum.v2.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/enum.v2.smithy index 56dbe6205be..c72d5e2883d 100644 --- a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/enum.v2.smithy +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/enum.v2.smithy @@ -3,8 +3,6 @@ $version: "2.0" namespace com.example enum EnumString { - @enumValue("foo") - FOO - @enumValue("bar") - BAR + FOO = "foo" + BAR = "bar" } diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/http-payload.v1.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/http-payload.v1.smithy index 47c830948b5..03ecfca79bc 100644 --- a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/http-payload.v1.smithy +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/http-payload.v1.smithy @@ -7,11 +7,6 @@ structure RequiredPayload { payload: StreamingBlob } -structure DefaultPayload { - @default - payload: StreamingBlob -} - structure NeitherRequiredNorDefaultPayload { payload: StreamingBlob } diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/http-payload.v2.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/http-payload.v2.smithy index 85629d30439..698212dadf7 100644 --- a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/http-payload.v2.smithy +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/http-payload.v2.smithy @@ -7,13 +7,8 @@ structure RequiredPayload { payload: StreamingBlob } -structure DefaultPayload { - @default - payload: StreamingBlob -} - structure NeitherRequiredNorDefaultPayload { - @default + @default("") payload: StreamingBlob } diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/multiple-enums.v2.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/multiple-enums.v2.smithy index aaad6ad6368..4a6947a7aef 100644 --- a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/multiple-enums.v2.smithy +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/multiple-enums.v2.smithy @@ -3,15 +3,11 @@ $version: "2.0" namespace com.example enum First { - @enumValue("foo") - FOO - @enumValue("bar") - BAR + FOO = "foo" + BAR = "bar" } enum Second { - @enumValue("foo") - FOO - @enumValue("bar") - BAR + FOO = "foo" + BAR = "bar" } diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v1.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v1.smithy index b792cfdaabe..b74b80278b8 100644 --- a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v1.smithy +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v1.smithy @@ -14,7 +14,7 @@ structure PrimitiveBearer { handlesComments: // Nobody actually does this right? PrimitiveShort, - @default + @default(0) handlesPreexistingDefault: PrimitiveShort, @required diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v2.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v2.smithy index a71cb70c565..9c4e41c1c10 100644 --- a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v2.smithy +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v2.smithy @@ -3,26 +3,26 @@ $version: "2.0" namespace com.example structure PrimitiveBearer { - @default + @default(0) int: Integer, - @default + @default(false) bool: Boolean, - @default + @default(0) byte: Byte, - @default + @default(0) double: Double, - @default + @default(0) float: Float, - @default + @default(0) long: Long, - @default + @default(0) short: Short, - @default + @default(0) handlesComments: // Nobody actually does this right? Short, - @default + @default(0) handlesPreexistingDefault: Short, @required diff --git a/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/ChangedNullabilityTest.java b/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/ChangedNullabilityTest.java index ba9eec9f1db..aaa0fd17940 100644 --- a/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/ChangedNullabilityTest.java +++ b/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/ChangedNullabilityTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test; import software.amazon.smithy.diff.ModelDiff; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.StringShape; import software.amazon.smithy.model.shapes.StructureShape; @@ -28,7 +29,7 @@ public void replacingRequiredTraitWithDefaultIsOk() { .build(); StructureShape b = StructureShape.builder() .id("smithy.example#A") - .addMember("foo", s.getId(), b2 -> b2.addTrait(new DefaultTrait())) + .addMember("foo", s.getId(), b2 -> b2.addTrait(new DefaultTrait(Node.from("")))) .build(); Model model1 = Model.builder().addShapes(s, a).build(); Model model2 = Model.builder().addShapes(s, b).build(); @@ -48,7 +49,7 @@ public void detectsInvalidAdditionOfDefaultTrait() { .build(); StructureShape b = StructureShape.builder() .id("smithy.example#A") - .addMember("foo", s.getId(), builder -> builder.addTrait(new DefaultTrait())) + .addMember("foo", s.getId(), builder -> builder.addTrait(new DefaultTrait(Node.from("")))) .build(); Model model1 = Model.builder().addShapes(s, a).build(); Model model2 = Model.builder().addShapes(s, b).build(); diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaShapeVisitor.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaShapeVisitor.java index 6e779149104..fe68c000809 100644 --- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaShapeVisitor.java +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaShapeVisitor.java @@ -42,6 +42,7 @@ import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.shapes.TimestampShape; import software.amazon.smithy.model.shapes.UnionShape; +import software.amazon.smithy.model.traits.DefaultTrait; import software.amazon.smithy.model.traits.DocumentationTrait; import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.LengthTrait; @@ -99,7 +100,9 @@ private Schema createRef(MemberShape member) { if (converter.isInlined(member)) { return member.accept(this); } else { - return Schema.builder().ref(converter.toPointer(member.getTarget())).build(); + Schema.Builder builder = Schema.builder().ref(converter.toPointer(member.getTarget())); + member.getTrait(DefaultTrait.class).ifPresent(trait -> builder.defaultValue(trait.toNode())); + return builder.build(); } } @@ -309,6 +312,9 @@ private Schema.Builder updateBuilder(Shape shape, Schema.Builder builder) { .map(EnumTrait::getEnumDefinitionValues) .ifPresent(builder::enumValues); + shape.getTrait(DefaultTrait.class) + .ifPresent(trait -> builder.defaultValue(trait.toNode())); + return builder; } diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java index f7b970a4ce2..ef2078a247a 100644 --- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java @@ -46,7 +46,6 @@ *

  • if
  • *
  • then
  • *
  • else
  • - *
  • default
  • *
  • examples
  • * * diff --git a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java index 3c6d83b123c..0e512c98c2c 100644 --- a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java +++ b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java @@ -647,4 +647,20 @@ public void removesMixins() { assertThat(properties.getValue(document.toNode()).expectObjectNode().getStringMap().keySet(), containsInAnyOrder("foo", "baz")); } + + @Test + public void appliesDefaults() { + Model model = Model.assembler() + .addImport(getClass().getResource("default-values.smithy")) + .assemble() + .unwrap(); + SchemaDocument document = JsonSchemaConverter.builder() + .model(model) + .build() + .convert(); + + Node expected = Node.parse( + IoUtils.toUtf8String(getClass().getResourceAsStream("default-values.jsonschema.json"))); + Node.assertEquals(document.toNode(), expected); + } } diff --git a/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/default-values.jsonschema.json b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/default-values.jsonschema.json new file mode 100644 index 00000000000..98d2d6e8fbb --- /dev/null +++ b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/default-values.jsonschema.json @@ -0,0 +1,24 @@ +{ + "definitions": { + "Foo": { + "type": "object", + "properties": { + "bam": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "bar": { + "type": "number", + "default": 0 + }, + "baz": { + "type": "string", + "default": "" + } + } + } + } +} diff --git a/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/default-values.smithy b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/default-values.smithy new file mode 100644 index 00000000000..cc44a5e32fd --- /dev/null +++ b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/default-values.smithy @@ -0,0 +1,13 @@ +$version: "2.0" + +namespace smithy.example + +structure Foo { + bar: Integer = 0 + baz: String = "" + bam: StringList = [] +} + +list StringList { + member: String +} diff --git a/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/missing-client-optional-onRequiredOrDefault.smithy b/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/missing-client-optional-onRequiredOrDefault.smithy index 6f7037ce3ef..e0b748c6647 100644 --- a/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/missing-client-optional-onRequiredOrDefault.smithy +++ b/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/missing-client-optional-onRequiredOrDefault.smithy @@ -12,8 +12,7 @@ metadata validators = [ namespace smithy.example structure Foo { - @default - bar: String, + bar: String = "" @required baz: String diff --git a/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/missing-client-optional-onRequiredStructureOrUnion.smithy b/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/missing-client-optional-onRequiredStructureOrUnion.smithy index 01511ebf1f7..02283c66a0c 100644 --- a/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/missing-client-optional-onRequiredStructureOrUnion.smithy +++ b/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/missing-client-optional-onRequiredStructureOrUnion.smithy @@ -12,8 +12,7 @@ metadata validators = [ namespace smithy.example structure Foo { - @default - bar: String, + bar: String = "", @required baz: String, diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java index 4d6e02c0605..f6aaf6dfaad 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java @@ -62,9 +62,12 @@ import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.shapes.TimestampShape; import software.amazon.smithy.model.shapes.UnionShape; +import software.amazon.smithy.model.traits.DefaultTrait; import software.amazon.smithy.model.traits.DocumentationTrait; +import software.amazon.smithy.model.traits.EnumValueTrait; import software.amazon.smithy.model.traits.InputTrait; import software.amazon.smithy.model.traits.OutputTrait; +import software.amazon.smithy.model.traits.Trait; import software.amazon.smithy.model.traits.TraitFactory; import software.amazon.smithy.model.traits.UnitTypeTrait; import software.amazon.smithy.model.validation.Severity; @@ -386,10 +389,10 @@ private void parseShape(List traits) { parseOperationStatement(id, location); break; case "structure": - parseStructuredShape(id, location, StructureShape.builder()); + parseStructuredShape(id, location, StructureShape.builder(), MemberParsing.PARSING_STRUCTURE_MEMBER); break; case "union": - parseStructuredShape(id, location, UnionShape.builder()); + parseStructuredShape(id, location, UnionShape.builder(), MemberParsing.PARSING_MEMBER); break; case "list": parseCollection(id, location, ListShape.builder()); @@ -407,7 +410,7 @@ private void parseShape(List traits) { parseSimpleShape(id, location, StringShape.builder()); break; case "enum": - parseEnumShape(id, location, EnumShape.builder()); + parseEnumShape(id, location, EnumShape.builder(), MemberParsing.PARSING_ENUM); break; case "blob": parseSimpleShape(id, location, BlobShape.builder()); @@ -422,7 +425,7 @@ private void parseShape(List traits) { parseSimpleShape(id, location, IntegerShape.builder()); break; case "intEnum": - parseEnumShape(id, location, IntEnumShape.builder()); + parseEnumShape(id, location, IntEnumShape.builder(), MemberParsing.PARSING_INT_ENUM); break; case "long": parseSimpleShape(id, location, LongShape.builder()); @@ -464,10 +467,15 @@ private void parseSimpleShape(ShapeId id, SourceLocation location, AbstractShape parseMixins(id); } - private void parseEnumShape(ShapeId id, SourceLocation location, AbstractShapeBuilder builder) { + private void parseEnumShape( + ShapeId id, + SourceLocation location, + AbstractShapeBuilder builder, + MemberParsing memberParsing + ) { modelFile.onShape(builder.id(id).source(location)); parseMixins(id); - parseMembers(id, Collections.emptySet(), true); + parseMembers(id, Collections.emptySet(), memberParsing); clearPendingDocs(); } @@ -482,10 +490,107 @@ private void parseCollection(ShapeId id, SourceLocation location, CollectionShap } private void parseMembers(ShapeId id, Set requiredMembers) { - parseMembers(id, requiredMembers, false); + parseMembers(id, requiredMembers, MemberParsing.PARSING_MEMBER); } - private void parseMembers(ShapeId id, Set requiredMembers, boolean targetsUnit) { + private enum MemberParsing { + PARSING_INT_ENUM { + @Override + boolean supportsAssignment() { + return true; + } + + @Override + Trait createAssignmentTrait(ShapeId id, Node value) { + NumberNode number = value.asNumberNode().orElseThrow(() -> ModelSyntaxException.builder() + .shapeId(id) + .sourceLocation(value) + .message("intEnum shapes require integer values but found: " + Node.printJson(value)) + .build()); + if (number.isFloatingPointNumber()) { + throw ModelSyntaxException.builder() + .shapeId(id) + .message("intEnum shapes do not support floating point values") + .sourceLocation(value) + .build(); + } + return EnumValueTrait.builder() + .sourceLocation(value.getSourceLocation()) + .intValue(number.getValue().intValue()) + .build(); + } + + @Override + boolean targetsUnit() { + return true; + } + }, + PARSING_ENUM { + @Override + boolean supportsAssignment() { + return true; + } + + @Override + Trait createAssignmentTrait(ShapeId id, Node value) { + String stringValue = value.asStringNode().orElseThrow(() -> ModelSyntaxException.builder() + .shapeId(id) + .sourceLocation(value) + .message("enum shapes require string values but found: " + Node.printJson(value)) + .build()) + .getValue(); + return EnumValueTrait.builder() + .sourceLocation(value.getSourceLocation()) + .stringValue(stringValue) + .build(); + } + + @Override + boolean targetsUnit() { + return true; + } + }, + PARSING_STRUCTURE_MEMBER { + @Override + boolean supportsAssignment() { + return true; + } + + @Override + Trait createAssignmentTrait(ShapeId id, Node value) { + return new DefaultTrait(value); + } + + @Override + boolean targetsUnit() { + return false; + } + }, + PARSING_MEMBER { + @Override + boolean supportsAssignment() { + return false; + } + + @Override + Trait createAssignmentTrait(ShapeId id, Node value) { + throw new UnsupportedOperationException(); + } + + @Override + boolean targetsUnit() { + return false; + } + }; + + abstract boolean supportsAssignment(); + + abstract Trait createAssignmentTrait(ShapeId id, Node value); + + abstract boolean targetsUnit(); + } + + private void parseMembers(ShapeId id, Set requiredMembers, MemberParsing memberParsing) { Set definedMembers = new HashSet<>(); ws(); @@ -497,7 +602,7 @@ private void parseMembers(ShapeId id, Set requiredMembers, boolean targe break; } - parseMember(id, requiredMembers, definedMembers, targetsUnit); + parseMember(id, requiredMembers, definedMembers, memberParsing); // Clears out any previously captured documentation // comments that may have been found when parsing the member. @@ -513,12 +618,12 @@ private void parseMembers(ShapeId id, Set requiredMembers, boolean targe expect('}'); } - private void parseMember(ShapeId parent, Set allowed, Set defined, boolean targetsUnit) { + private void parseMember(ShapeId parent, Set allowed, Set defined, MemberParsing memberParsing) { // Parse optional member traits. List memberTraits = parseDocsAndTraits(); SourceLocation memberLocation = currentLocation(); - boolean isTargetElided = !targetsUnit && peek() == '$'; + boolean isTargetElided = !memberParsing.targetsUnit() && peek() == '$'; if (isTargetElided) { expect('$'); } @@ -550,21 +655,28 @@ private void parseMember(ShapeId parent, Set allowed, Set define // Members whose targets are elided will have those targets resolved later, // for example by SetResourceBasedTargets if (!isTargetElided) { - if (!targetsUnit) { + if (memberParsing.targetsUnit()) { + modelFile.addForwardReference(UnitTypeTrait.UNIT.toString(), memberBuilder::target); + } else { ws(); expect(':'); - - if (peek() == '=') { - throw syntax("Defining structures inline with the `:=` syntax may only be used when " - + "defining operation input and output shapes."); - } - ws(); modelFile.addForwardReference(ParserUtils.parseShapeId(this), memberBuilder::target); - } else { - modelFile.addForwardReference(UnitTypeTrait.UNIT.toString(), memberBuilder::target); } } + + // Skip spaces to check if there is default trait sugar. + sp(); + + if (memberParsing.supportsAssignment() && peek() == '=') { + if (!modelFile.getVersion().isDefaultSupported()) { + throw syntax("@default assignment is only supported in IDL version 2 or later"); + } + expect('='); + ws(); + memberBuilder.addTrait(memberParsing.createAssignmentTrait(memberId, IdlNodeParser.parseNode(this))); + } + addTraits(parent.withMember(memberName), memberTraits); } @@ -585,7 +697,8 @@ private void parseMapStatement(ShapeId id, SourceLocation location) { private void parseStructuredShape( ShapeId id, SourceLocation location, - AbstractShapeBuilder builder + AbstractShapeBuilder builder, + MemberParsing memberParsing ) { // Register the structure/union with the loader before parsing members. // This will detect shape conflicts with other types (like an operation) @@ -602,7 +715,7 @@ private void parseStructuredShape( // Parse optional "with" statements to add mixins, but only if it's supported by the version. parseMixins(id); - parseMembers(id, Collections.emptySet()); + parseMembers(id, Collections.emptySet(), memberParsing); clearPendingDocs(); } @@ -714,7 +827,7 @@ private ShapeId parseInlineStructure(String name, TraitEntry defaultTrait) { StructureShape.Builder builder = StructureShape.builder().id(id).source(location); modelFile.onShape(builder); - parseMembers(id, Collections.emptySet()); + parseMembers(id, Collections.emptySet(), MemberParsing.PARSING_STRUCTURE_MEMBER); addTraits(id, traits); clearPendingDocs(); ws(); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java index 8d9e21de260..9b7cf92bc81 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java @@ -22,6 +22,9 @@ import java.util.Map; import java.util.Set; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.BooleanNode; +import software.amazon.smithy.model.node.NumberNode; +import software.amazon.smithy.model.node.StringNode; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; @@ -125,7 +128,14 @@ private void upgradeV1Member(ShapeType containerType, MemberShape member) { + "Smithy IDL 2.0") .build()); builder = createOrReuseBuilder(member, builder); - builder.addTrait(new DefaultTrait(builder.getSourceLocation())); + + if (target.isBooleanShape()) { + builder.addTrait(new DefaultTrait(new BooleanNode(false, builder.getSourceLocation()))); + } else if (target.isBlobShape()) { + builder.addTrait(new DefaultTrait(new StringNode("", builder.getSourceLocation()))); + } else { + builder.addTrait(new DefaultTrait(new NumberNode(0, builder.getSourceLocation()))); + } } if (builder != null) { diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/Version.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/Version.java index 1090abfecc1..1a60fa90310 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/Version.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/Version.java @@ -120,4 +120,8 @@ void validateVersionedTrait(ShapeId target, ShapeId traitId, Node value) { .build(); } } + + boolean isDefaultSupported() { + return this == VERSION_2_0; + } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/node/internal/NodeHandler.java b/smithy-model/src/main/java/software/amazon/smithy/model/node/internal/NodeHandler.java index d20bb30e7ab..a0357e03ca0 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/node/internal/NodeHandler.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/node/internal/NodeHandler.java @@ -77,8 +77,9 @@ void endNumber(String string, SourceLocation location) { double doubleValue = Double.parseDouble(string); if (Double.isFinite(doubleValue)) { value = new NumberNode(doubleValue, location); + } else { + value = new NumberNode(new BigDecimal(string), location); } - value = new NumberNode(new BigDecimal(string), location); } else { try { value = new NumberNode(Long.parseLong(string), location); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/ShapeTypeSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/ShapeTypeSelector.java index 3cbdbf9af2f..7f6b34e8c96 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/ShapeTypeSelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/ShapeTypeSelector.java @@ -31,19 +31,13 @@ final class ShapeTypeSelector implements InternalSelector { @Override public boolean push(Context ctx, Shape shape, Receiver next) { - if (shape.getType().isShapeType(shapeType) || isSetMatchForList(shape)) { + if (shape.getType().isShapeType(shapeType)) { return next.apply(ctx, shape); } return true; } - private boolean isSetMatchForList(Shape shape) { - ShapeType other = shape.getType(); - return (shapeType == ShapeType.SET && other == ShapeType.LIST) - || (shapeType == ShapeType.LIST && other == ShapeType.SET); - } - @Override public Function> optimize() { return model -> model.toSet(shapeType.getShapeClass()); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java index 35d0df0411a..03a4726b50f 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java @@ -27,7 +27,6 @@ import software.amazon.smithy.model.SourceException; import software.amazon.smithy.model.traits.DeprecatedTrait; import software.amazon.smithy.model.traits.DocumentationTrait; -import software.amazon.smithy.model.traits.EnumDefaultTrait; import software.amazon.smithy.model.traits.EnumDefinition; import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.EnumValueTrait; @@ -66,10 +65,6 @@ public Map getEnumValues() { if (enumValues == null) { Map values = new LinkedHashMap<>(members.size()); for (MemberShape member : members()) { - if (member.hasTrait(EnumDefaultTrait.ID)) { - values.put(member.getMemberName(), ""); - continue; - } values.put(member.getMemberName(), member.expectTrait(EnumValueTrait.class).expectStringValue()); } enumValues = Collections.unmodifiableMap(values); @@ -311,15 +306,11 @@ static boolean canConvertEnumDefinitionToMember(EnumDefinition definition, boole static EnumDefinition enumDefinitionFromMember(MemberShape member) { EnumDefinition.Builder builder = EnumDefinition.builder().name(member.getMemberName()); - Optional traitValue = member.getTrait(EnumValueTrait.class).flatMap(EnumValueTrait::getStringValue); - if (member.hasTrait(EnumDefaultTrait.class)) { - builder.value(""); - } else if (traitValue.isPresent()) { - builder.value(traitValue.get()); - } else { - throw new IllegalStateException("Enum definitions can only be made for string enums."); - } - + String traitValue = member + .getTrait(EnumValueTrait.class) + .flatMap(EnumValueTrait::getStringValue) + .orElseThrow(() -> new IllegalStateException("Enum definitions can only be made for string enums.")); + builder.value(traitValue); member.getTrait(DocumentationTrait.class).ifPresent(docTrait -> builder.documentation(docTrait.getValue())); member.getTrait(TagsTrait.class).ifPresent(tagsTrait -> builder.tags(tagsTrait.getValues())); member.getTrait(DeprecatedTrait.class).ifPresent(deprecatedTrait -> builder.deprecated(true)); @@ -460,7 +451,7 @@ public Builder addMember(MemberShape member) { "Enum members may only target `smithy.api#Unit`, but found `%s`", member.getTarget() ), getSourceLocation()); } - if (!member.hasTrait(EnumValueTrait.ID) && !member.hasTrait(EnumDefaultTrait.ID)) { + if (!member.hasTrait(EnumValueTrait.ID)) { member = member.toBuilder() .addTrait(EnumValueTrait.builder().stringValue(member.getMemberName()).build()) .build(); @@ -506,40 +497,6 @@ public Builder addMember(String memberName, String enumValue, Consumer memberUpdater) { - if (getId() == null) { - throw new IllegalStateException("An id must be set before setting a member with a target"); - } - - MemberShape.Builder builder = MemberShape.builder() - .target(UnitTypeTrait.UNIT) - .id(getId().withMember(memberName)) - .addTrait(new EnumDefaultTrait()); - - if (memberUpdater != null) { - memberUpdater.accept(builder); - } - - return addMember(builder.build()); - } - /** * Removes a member by name. * diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java index ef7b799d930..23987168c8b 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java @@ -23,7 +23,6 @@ import java.util.Optional; import java.util.function.Consumer; import software.amazon.smithy.model.SourceException; -import software.amazon.smithy.model.traits.EnumDefaultTrait; import software.amazon.smithy.model.traits.EnumValueTrait; import software.amazon.smithy.model.traits.UnitTypeTrait; import software.amazon.smithy.utils.BuilderRef; @@ -54,10 +53,6 @@ public Map getEnumValues() { if (enumValues == null) { Map values = new LinkedHashMap<>(members.size()); for (MemberShape member : members()) { - if (member.hasTrait(EnumDefaultTrait.ID)) { - values.put(member.getMemberName(), 0); - continue; - } values.put(member.getMemberName(), member.expectTrait(EnumValueTrait.class).expectIntValue()); } enumValues = Collections.unmodifiableMap(values); @@ -240,40 +235,6 @@ public Builder addMember(String memberName, int enumValue, Consumer memberUpdater) { - if (getId() == null) { - throw new IllegalStateException("An id must be set before setting a member with a target"); - } - - MemberShape.Builder builder = MemberShape.builder() - .target(UnitTypeTrait.UNIT) - .id(getId().withMember(memberName)) - .addTrait(new EnumDefaultTrait()); - - if (memberUpdater != null) { - memberUpdater.accept(builder); - } - - return addMember(builder.build()); - } - /** * Removes a member by name. * diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java index a6d4f805383..62b0470f0f4 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java @@ -15,8 +15,8 @@ package software.amazon.smithy.model.shapes; +import java.util.Objects; import java.util.Optional; -import java.util.function.BiFunction; /** An enumeration of the different types in a model. */ public enum ShapeType { @@ -34,12 +34,30 @@ public enum ShapeType { DOUBLE("double", DoubleShape.class, Category.SIMPLE), BIG_DECIMAL("bigDecimal", BigDecimalShape.class, Category.SIMPLE), BIG_INTEGER("bigInteger", BigIntegerShape.class, Category.SIMPLE), - - ENUM("enum", EnumShape.class, Category.SIMPLE, (a, b) -> b == a || b == STRING), - INT_ENUM("intEnum", IntEnumShape.class, Category.SIMPLE, (a, b) -> b == a || b == INTEGER), - - LIST("list", ListShape.class, Category.AGGREGATE), - SET("set", SetShape.class, Category.AGGREGATE), + ENUM("enum", EnumShape.class, Category.SIMPLE) { + @Override + public boolean isShapeType(ShapeType other) { + return this == other || other == STRING; + } + }, + INT_ENUM("intEnum", IntEnumShape.class, Category.SIMPLE) { + @Override + public boolean isShapeType(ShapeType other) { + return this == other || other == INTEGER; + } + }, + LIST("list", ListShape.class, Category.AGGREGATE) { + @Override + public boolean isShapeType(ShapeType other) { + return this == other || other == SET; + } + }, + SET("set", SetShape.class, Category.AGGREGATE) { + @Override + public boolean isShapeType(ShapeType other) { + return this == other || other == LIST; + } + }, MAP("map", MapShape.class, Category.AGGREGATE), STRUCTURE("structure", StructureShape.class, Category.AGGREGATE), UNION("union", UnionShape.class, Category.AGGREGATE), @@ -55,22 +73,11 @@ public enum Category { SIMPLE, AGGREGATE, SERVICE, MEMBER } private final String stringValue; private final Class shapeClass; private final Category category; - private final BiFunction isShapeType; ShapeType(String stringValue, Class shapeClass, Category category) { - this(stringValue, shapeClass, category, Enum::equals); - } - - ShapeType( - String stringValue, - Class shapeClass, - Category category, - BiFunction isShapeType - ) { this.stringValue = stringValue; this.shapeClass = shapeClass; this.category = category; - this.isShapeType = isShapeType; } @Override @@ -98,15 +105,14 @@ public Category getCategory() { } /** - * Returns whether this shape type is equivalent to the given shape type. - * - * This accounts for things like enums being considered specializations of strings. + * Returns whether this shape type is equivalent to the given shape type, + * accounting for this like enums being considered specializations of strings. * * @param other The other shape type to compare against. * @return Returns true if the shape types are equivalent. */ public boolean isShapeType(ShapeType other) { - return isShapeType.apply(this, other); + return Objects.equals(this, other); } /** diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java index 8bd3401d88a..5154ee778c0 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java @@ -43,6 +43,7 @@ import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.node.StringNode; import software.amazon.smithy.model.traits.AnnotationTrait; +import software.amazon.smithy.model.traits.DefaultTrait; import software.amazon.smithy.model.traits.DocumentationTrait; import software.amazon.smithy.model.traits.EnumValueTrait; import software.amazon.smithy.model.traits.IdRefTrait; @@ -465,7 +466,11 @@ private void writeShapeMembers(Collection members) { codeWriter.openBlock("{", "}", () -> { for (MemberShape member : members) { serializeTraits(member.getAllTraits()); - codeWriter.write("$L: $I", member.getMemberName(), member.getTarget()); + String assignment = ""; + if (member.hasTrait(DefaultTrait.class)) { + assignment = " = " + Node.printJson(member.expectTrait(DefaultTrait.class).toNode()); + } + codeWriter.write("$L: $I$L", member.getMemberName(), member.getTarget(), assignment); } }); } @@ -481,11 +486,14 @@ private void writeEnumMembers(Collection members) { for (MemberShape member : members) { Map traits = new LinkedHashMap<>(member.getAllTraits()); Optional stringValue = member.expectTrait(EnumValueTrait.class).getStringValue(); - if (stringValue.isPresent() && member.getMemberName().equals(stringValue.get())) { - traits.remove(EnumValueTrait.ID); + boolean hasNormalName = stringValue.isPresent() && member.getMemberName().equals(stringValue.get()); + String assignment = ""; + if (!hasNormalName) { + assignment = " = " + Node.printJson(member.expectTrait(EnumValueTrait.class).toNode()); } + traits.remove(EnumValueTrait.ID); serializeTraits(traits); - codeWriter.write("$L", member.getMemberName()); + codeWriter.write("$L$L", member.getMemberName(), assignment); } }); } @@ -523,6 +531,8 @@ private void serializeTraits(Map traits, TraitFeature... traitFe traits.values().stream() .filter(trait -> noSpecialDocsSyntax || !(trait instanceof DocumentationTrait)) + // The default and enumValue traits are serialized using the assignment syntactic sugar. + .filter(trait -> !(trait instanceof DefaultTrait) && !(trait instanceof EnumValueTrait)) .filter(traitFilter) .sorted(Comparator.comparing(Trait::toShapeId)) .forEach(this::serializeTrait); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/DefaultTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/DefaultTrait.java index 2261dfba22b..4e25c5713f4 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/DefaultTrait.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/DefaultTrait.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. @@ -15,33 +15,33 @@ package software.amazon.smithy.model.traits; -import java.util.Collections; -import software.amazon.smithy.model.SourceLocation; import software.amazon.smithy.model.node.Node; -import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.ShapeId; /** * Indicates that a shape is defaulted, meaning a zero value is used when one is not provided. */ -public final class DefaultTrait extends AnnotationTrait { - public static final ShapeId ID = ShapeId.from("smithy.api#default"); +public final class DefaultTrait extends AbstractTrait { - public DefaultTrait(ObjectNode node) { - super(ID, node); - } + public static final ShapeId ID = ShapeId.from("smithy.api#default"); - public DefaultTrait(SourceLocation location) { - this(new ObjectNode(Collections.emptyMap(), location)); + public DefaultTrait(Node value) { + super(ID, value); } - public DefaultTrait() { - this(Node.objectNode()); + @Override + protected Node createNode() { + throw new UnsupportedOperationException("NodeCache is always set"); } - public static final class Provider extends AnnotationTrait.Provider { + public static final class Provider extends AbstractTrait.Provider { public Provider() { - super(ID, DefaultTrait::new); + super(ID); + } + + @Override + public Trait createTrait(ShapeId target, Node value) { + return new DefaultTrait(value); } } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefaultTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefaultTrait.java deleted file mode 100644 index c4142879a02..00000000000 --- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefaultTrait.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.smithy.model.traits; - -import java.util.Collections; -import software.amazon.smithy.model.SourceLocation; -import software.amazon.smithy.model.node.Node; -import software.amazon.smithy.model.node.ObjectNode; -import software.amazon.smithy.model.shapes.EnumShape; -import software.amazon.smithy.model.shapes.IntEnumShape; -import software.amazon.smithy.model.shapes.ShapeId; - -/** - * Indicates that an enum member is the default value member. - * - * On an {@link IntEnumShape} this implies the enum value is 0. On an - * {@link EnumShape} this implies the enum value is an empty string. - */ -public final class EnumDefaultTrait extends AnnotationTrait { - public static final ShapeId ID = ShapeId.from("smithy.api#enumDefault"); - - public EnumDefaultTrait(ObjectNode node) { - super(ID, node); - } - - public EnumDefaultTrait(SourceLocation location) { - this(new ObjectNode(Collections.emptyMap(), location)); - } - - public EnumDefaultTrait() { - this(Node.objectNode()); - } - - public static final class Provider extends AnnotationTrait.Provider { - public Provider() { - super(ID, EnumDefaultTrait::new); - } - } -} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/DefaultTraitValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/DefaultTraitValidator.java new file mode 100644 index 00000000000..77167b8a664 --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/DefaultTraitValidator.java @@ -0,0 +1,93 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.validation.validators; + +import java.util.ArrayList; +import java.util.List; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.traits.DefaultTrait; +import software.amazon.smithy.model.validation.AbstractValidator; +import software.amazon.smithy.model.validation.NodeValidationVisitor; +import software.amazon.smithy.model.validation.ValidationEvent; +import software.amazon.smithy.utils.SmithyInternalApi; + +@SmithyInternalApi +public final class DefaultTraitValidator extends AbstractValidator { + @Override + public List validate(Model model) { + List events = new ArrayList<>(); + NodeValidationVisitor visitor = null; + + // Validate that default values are appropriate for shapes. + for (MemberShape shape : model.getMemberShapesWithTrait(DefaultTrait.class)) { + DefaultTrait trait = shape.expectTrait(DefaultTrait.class); + Node value = trait.toNode(); + + if (visitor == null) { + visitor = NodeValidationVisitor + .builder() + .model(model) + .eventId(getName()) + .value(value) + .startingContext("Error validating @default trait") + .eventShapeId(shape.getId()) + .build(); + } else { + visitor.setValue(value); + visitor.setEventShapeId(shape.getId()); + } + + events.addAll(shape.accept(visitor)); + switch (model.expectShape(shape.getTarget()).getType()) { + case MAP: + value.asObjectNode().ifPresent(obj -> { + if (!obj.isEmpty()) { + events.add(error(shape, trait, "The @default value of a map must be an empty map")); + } + }); + break; + case LIST: + case SET: + value.asArrayNode().ifPresent(array -> { + if (!array.isEmpty()) { + events.add(error(shape, trait, "The @default value of a list must be an empty list")); + } + }); + break; + case DOCUMENT: + value.asArrayNode().ifPresent(array -> { + if (!array.isEmpty()) { + events.add(error(shape, trait, "The @default value of a document cannot be an non-empty " + + "array")); + } + }); + value.asObjectNode().ifPresent(obj -> { + if (!obj.isEmpty()) { + events.add(error(shape, trait, "The @default value of a document cannot be a non-empty " + + "object")); + } + }); + break; + default: + break; + } + } + + return events; + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java index da134af6739..78c1894f517 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java @@ -27,7 +27,6 @@ import software.amazon.smithy.model.shapes.IntEnumShape; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.model.traits.EnumDefaultTrait; import software.amazon.smithy.model.traits.EnumValueTrait; import software.amazon.smithy.model.validation.AbstractValidator; import software.amazon.smithy.model.validation.ValidationEvent; @@ -35,8 +34,7 @@ /** * Emits an error validation event if an enum member's enumValue trait has the wrong type, * if there are any duplicate values in a single enum, if the enum's default value is - * set using the enumValue trait, or if an intEnum member lacks an enumValue / enumDefault - * trait. + * set using the enumValue trait, or if an intEnum member lacks an enumValue trait. * *

    Additionally, emits warning events when enum member names don't follow the recommended * naming convention of all upper case letters separated by underscores. @@ -62,9 +60,6 @@ public List validate(Model model) { private void validateEnumShape(List events, EnumShape shape) { Set values = new HashSet<>(); for (MemberShape member : shape.members()) { - if (member.hasTrait(EnumDefaultTrait.ID)) { - continue; - } Optional value = member.expectTrait(EnumValueTrait.class).getStringValue(); if (!value.isPresent()) { events.add(error(member, member.expectTrait(EnumValueTrait.class), @@ -77,9 +72,7 @@ private void validateEnumShape(List events, EnumShape shape) { ))); } if (value.get().equals("")) { - events.add(error(member, "enum values may not be empty because an empty string is the " - + "default value of enum shapes. Instead, use `smithy.api#enumDefault` to set an " - + "explicit name for the default value.")); + events.add(error(member, "enum values may not be empty.")); } } validateEnumMemberName(events, member); @@ -89,9 +82,6 @@ private void validateEnumShape(List events, EnumShape shape) { private void validateIntEnumShape(List events, IntEnumShape shape) { Set values = new HashSet<>(); for (MemberShape member : shape.members()) { - if (member.hasTrait(EnumDefaultTrait.ID)) { - continue; - } if (!member.hasTrait(EnumValueTrait.ID)) { events.add(missingIntEnumValue(member, member)); } else if (!member.expectTrait(EnumValueTrait.class).getIntValue().isPresent()) { @@ -104,11 +94,6 @@ private void validateIntEnumShape(List events, IntEnumShape sha value ))); } - if (value == 0) { - events.add(error(member, "intEnum values may not be set to 0 because 0 is the " - + "default value of intEnum shapes. Instead, use `smithy.api#enumDefault` to set an " - + "explicit name for the default value.")); - } } validateEnumMemberName(events, member); } diff --git a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService index 75aa0cc507f..dcca0bf6d6e 100644 --- a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService +++ b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService @@ -6,7 +6,6 @@ software.amazon.smithy.model.traits.DefaultTrait$Provider software.amazon.smithy.model.traits.DeprecatedTrait$Provider software.amazon.smithy.model.traits.DocumentationTrait$Provider software.amazon.smithy.model.traits.EndpointTrait$Provider -software.amazon.smithy.model.traits.EnumDefaultTrait$Provider software.amazon.smithy.model.traits.EnumTrait$Provider software.amazon.smithy.model.traits.EnumValueTrait$Provider software.amazon.smithy.model.traits.ErrorTrait$Provider diff --git a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator index 2557d9fb6e9..9872e02937a 100644 --- a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator +++ b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator @@ -1,5 +1,6 @@ software.amazon.smithy.model.validation.validators.AuthTraitValidator software.amazon.smithy.model.validation.validators.DefaultValueInUpdateValidator +software.amazon.smithy.model.validation.validators.DefaultTraitValidator software.amazon.smithy.model.validation.validators.DeprecatedTraitValidator software.amazon.smithy.model.validation.validators.EnumShapeValidator software.amazon.smithy.model.validation.validators.EnumTraitValidator diff --git a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy index 69565e0a3d5..751d4c84581 100644 --- a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy +++ b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy @@ -71,7 +71,7 @@ structure TraitDiffRule { change: TraitChangeType, /// Defines the severity of the change. Defaults to ERROR if not defined. - severity: TraitChangeSeverity, + severity: TraitChangeSeverity = "ERROR", /// Provides a reason why the change is potentially backward incompatible. message: String @@ -80,24 +80,19 @@ structure TraitDiffRule { @private enum TraitChangeType { /// Emit when a trait already existed, continues to exist, but it is modified. - @enumValue("update") - UPDATE + UPDATE = "update" /// Emit when a trait or value is added that previously did not exist - @enumValue("add") - ADD + ADD = "add" /// Emit when a trait or value is removed. - @enumValue("remove") - REMOVE + REMOVE = "remove" /// Emit when a trait is added or removed. - @enumValue("presence") - PRESENCE + PRESENCE = "presence" /// Emit when any change occurs. - @enumValue("any") - ANY + ANY = "any" } @private @@ -118,12 +113,10 @@ enum TraitChangeSeverity { @private enum StructurallyExclusive { /// Only a single member of a shape can be marked with the trait. - @enumValue("member") - MEMBER + MEMBER = "member" /// Only a single member of a shape can target a shape marked with this trait. - @enumValue("target") - TARGET + TARGET = "target" } /// Marks a shape or member as deprecated. @@ -253,15 +246,17 @@ structure httpApiKeyAuth { scheme: NonEmptyString, } -/// Provides a structure member with a default zero value. +/// Provides a structure member with a default value. @trait( selector: "structure > member :test(> :is(simpleType, collection, map))", - conflicts: [required], // The default trait can never be removed. It can only be added if the - // member was previously marked as required. - breakingChanges: [{change: "remove"}] + // member is or was previously marked as required. + breakingChanges: [ + {change: "remove"}, + {change: "update", severity: "DANGER", message: "Default values should only be changed when absolutely necessary."} + ] ) -structure default {} +document default /// Requires that non-authoritative generators like clients treat a structure member as /// nullable regardless of if the member is also marked with the required trait. @@ -270,11 +265,8 @@ structure clientOptional {} @private enum HttpApiKeyLocations { - @enumValue("header") - HEADER - - @enumValue("query") - QUERY + HEADER = "header" + QUERY = "query" } /// Indicates that an operation can be called without authentication. @@ -322,11 +314,8 @@ structure ExampleError { breakingChanges: [{change: "any"}] ) enum error { - @enumValue("client") - CLIENT - - @enumValue("server") - SERVER + CLIENT = "client" + SERVER = "server" } /// Indicates that an error MAY be retried by the client. @@ -579,15 +568,6 @@ string EnumConstantBodyName @tags(["diff.error.const"]) document enumValue -/// Sets an enum member as the default value member. -@trait( - selector: ":is(enum, intEnum) > member" - structurallyExclusive: "member" - conflicts: [enumValue] -) -@tags(["diff.error.const"]) -structure enumDefault {} - /// Constrains a shape to minimum and maximum number of elements or size. @trait(selector: ":test(list, map, string, blob, member > :is(list, map, string, blob))") structure length { @@ -723,10 +703,8 @@ structure http { uri: NonEmptyString, /// The HTTP status code of a successful response. - /// - /// Defaults to 200 if not provided. @range(min: 100, max: 999) - code: Integer, + code: Integer = 200, } /// Binds an operation input structure member to an HTTP label. @@ -816,16 +794,14 @@ structure httpResponseCode {} ) structure cors { /// The origin from which browser script-originating requests will be allowed. - /// - /// Defaults to *. - origin: NonEmptyString, + origin: NonEmptyString = "*", /// The maximum number of seconds for which browsers are allowed to cache /// the results of a preflight OPTIONS request. /// /// Defaults to 600, the maximum age permitted by several browsers. /// Set to -1 to disable caching entirely. - maxAge: Integer, + maxAge: Integer = 600, /// The names of headers that should be included in the /// Access-Control-Allow-Headers header in responses to preflight OPTIONS @@ -876,9 +852,7 @@ structure eventHeader {} @trait(selector: ":test(string, member > string)") structure idRef { /// Defines the selector that the resolved shape, if found, MUST match. - /// - /// selector defaults to * when not defined. - selector: String, + selector: String = "*", /// When set to `true`, the shape ID MUST target a shape that can be /// found in the model. @@ -899,19 +873,16 @@ enum timestampFormat { /// Date time as defined by the date-time production in RFC3339 section 5.6 /// with no UTC offset (for example, 1985-04-12T23:20:50.52Z). - @enumValue("date-time") - DATE_TIME + DATE_TIME = "date-time" /// Also known as Unix time, the number of seconds that have elapsed since /// 00:00:00 Coordinated Universal Time (UTC), Thursday, 1 January 1970, /// with decimal precision (for example, 1515531081.1234). - @enumValue("epoch-seconds") - EPOCH_SECONDS + EPOCH_SECONDS = "epoch-seconds" /// An HTTP date as defined by the IMF-fixdate production in /// RFC 7231#section-7.1.1.1 (for example, Tue, 29 Apr 2014 18:30:38 GMT). - @enumValue("http-date") - HTTP_DATE + HTTP_DATE = "http-date" } /// Configures a custom operation endpoint. diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java index 89f856b9c21..2e974bc7ff6 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java @@ -26,6 +26,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.shapes.ListShape; import software.amazon.smithy.model.shapes.MapShape; import software.amazon.smithy.model.shapes.SetShape; @@ -34,9 +35,9 @@ import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.shapes.UnionShape; import software.amazon.smithy.model.traits.BoxTrait; +import software.amazon.smithy.model.traits.ClientOptionalTrait; import software.amazon.smithy.model.traits.DefaultTrait; import software.amazon.smithy.model.traits.InputTrait; -import software.amazon.smithy.model.traits.ClientOptionalTrait; import software.amazon.smithy.model.traits.RequiredTrait; import software.amazon.smithy.model.traits.SparseTrait; @@ -174,14 +175,14 @@ public void takesNullableIntoAccount( // This member is technically invalid, but nullable takes precedent here // over the default trait. .addMember("foo", str.getId(), b -> b.addTrait(new ClientOptionalTrait()) - .addTrait(new DefaultTrait()) + .addTrait(new DefaultTrait(Node.from("a"))) .build()) .addMember("bar", str.getId(), b -> b.addTrait(new ClientOptionalTrait()) .addTrait(new RequiredTrait()) .build()) .addMember("baz", str.getId(), b -> b.addTrait(new ClientOptionalTrait()).build()) .addMember("bam", str.getId(), b -> b.addTrait(new RequiredTrait()).build()) - .addMember("boo", str.getId(), b -> b.addTrait(new DefaultTrait()).build()) + .addMember("boo", str.getId(), b -> b.addTrait(new DefaultTrait(Node.from("boo"))).build()) .build(); Model model = Model.builder().addShapes(str, struct).build(); @@ -208,7 +209,7 @@ public void takesInputTraitIntoAccount(NullableIndex.CheckMode mode, boolean foo StructureShape struct = StructureShape.builder() .id("smithy.example#Struct") .addTrait(new InputTrait()) - .addMember("foo", str.getId(), b -> b.addTrait(new DefaultTrait()).build()) + .addMember("foo", str.getId(), b -> b.addTrait(new DefaultTrait(Node.from("foo"))).build()) .addMember("bar", str.getId(), b -> b.addTrait(new RequiredTrait()).build()) .addMember("baz", str.getId()) .build(); diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/node/NodeParserTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/node/NodeParserTest.java index d65ddc3d812..d1b6d5b97dd 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/node/NodeParserTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/node/NodeParserTest.java @@ -81,7 +81,7 @@ public void parsesFloatNode() { Node result = Node.parse("1.5"); assertThat(result.isNumberNode(), is(true)); - assertThat(result.expectNumberNode().getValue(), equalTo(new BigDecimal("1.5"))); + assertThat(result.expectNumberNode().getValue(), equalTo(1.5)); assertThat(result.getSourceLocation().getLine(), is(1)); assertThat(result.getSourceLocation().getColumn(), is(1)); } diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java index e7293735b58..ff427f3cf92 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java @@ -15,7 +15,9 @@ package software.amazon.smithy.model.shapes; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Map; import java.util.Optional; @@ -24,7 +26,6 @@ import software.amazon.smithy.model.SourceException; import software.amazon.smithy.model.traits.DeprecatedTrait; import software.amazon.smithy.model.traits.DocumentationTrait; -import software.amazon.smithy.model.traits.EnumDefaultTrait; import software.amazon.smithy.model.traits.EnumDefinition; import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.EnumValueTrait; @@ -74,26 +75,6 @@ public void addMember() { )); } - @Test - public void addDefaultMember() { - EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); - EnumShape shape = builder.addDefaultMember("foo").build(); - assertEquals(shape.getMember("foo").get(), - MemberShape.builder() - .id(shape.getId().withMember("foo")) - .target(UnitTypeTrait.UNIT) - .addTrait(new EnumDefaultTrait()) - .build()); - - assertTrue(shape.hasTrait(EnumTrait.class)); - assertEquals(shape.expectTrait(EnumTrait.class).getValues(), ListUtils.of( - EnumDefinition.builder() - .name("foo") - .value("") - .build() - )); - } - @Test public void addMemberShape() { EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); @@ -459,12 +440,9 @@ public void cantConvertBaseStringWithNamelessEnumTrait() { @Test public void getEnumValues() { EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); - EnumShape shape = builder.addDefaultMember("FOO").addMember("BAR", "bar").build(); + EnumShape shape = builder.addMember("BAR", "bar").build(); - Map expected = MapUtils.of( - "FOO", "", - "BAR", "bar" - ); + Map expected = MapUtils.of("BAR", "bar"); assertEquals(expected, shape.getEnumValues()); } } diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/IntEnumShapeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/IntEnumShapeTest.java index bb0f27a597f..afe35a349fe 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/IntEnumShapeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/IntEnumShapeTest.java @@ -15,13 +15,14 @@ package software.amazon.smithy.model.shapes; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Map; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import software.amazon.smithy.model.SourceException; -import software.amazon.smithy.model.traits.EnumDefaultTrait; import software.amazon.smithy.model.traits.EnumValueTrait; import software.amazon.smithy.model.traits.UnitTypeTrait; import software.amazon.smithy.utils.MapUtils; @@ -58,18 +59,6 @@ public void addMember() { .build()); } - @Test - public void addDefaultMember() { - IntEnumShape.Builder builder = (IntEnumShape.Builder) IntEnumShape.builder().id("ns.foo#bar"); - IntEnumShape shape = builder.addDefaultMember("foo").build(); - assertEquals(shape.getMember("foo").get(), - MemberShape.builder() - .id(shape.getId().withMember("foo")) - .target(UnitTypeTrait.UNIT) - .addTrait(new EnumDefaultTrait()) - .build()); - } - @Test public void addMemberShape() { IntEnumShape.Builder builder = (IntEnumShape.Builder) IntEnumShape.builder().id("ns.foo#bar"); @@ -160,12 +149,9 @@ public void membersMustTargetUnit() { @Test public void getEnumValues() { IntEnumShape.Builder builder = (IntEnumShape.Builder) IntEnumShape.builder().id("ns.foo#bar"); - IntEnumShape shape = builder.addDefaultMember("FOO").addMember("BAR", 1).build(); + IntEnumShape shape = builder.addMember("FOO", 0).addMember("BAR", 1).build(); - Map expected = MapUtils.of( - "FOO", 0, - "BAR", 1 - ); + Map expected = MapUtils.of("FOO", 0, "BAR", 1); assertEquals(expected, shape.getEnumValues()); } } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/document-must-be-singular.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/document-must-be-singular.errors new file mode 100644 index 00000000000..b3283247ceb --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/document-must-be-singular.errors @@ -0,0 +1,2 @@ +[ERROR] smithy.example#Foo$f: The @default value of a document cannot be an non-empty array | DefaultTrait +[ERROR] smithy.example#Foo$g: The @default value of a document cannot be a non-empty object | DefaultTrait diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/document-must-be-singular.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/document-must-be-singular.smithy new file mode 100644 index 00000000000..885f8a1aa52 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/document-must-be-singular.smithy @@ -0,0 +1,13 @@ +$version: "2.0" + +namespace smithy.example + +structure Foo { + a: Document = 0 + b: Document = true + c: Document = "hello" + d: Document = [] + e: Document = {} + f: Document = [1] + g: Document = {foo: true} +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/enum-must-match-values.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/enum-must-match-values.errors new file mode 100644 index 00000000000..6c6c439177c --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/enum-must-match-values.errors @@ -0,0 +1 @@ +[ERROR] smithy.example#Foo$baz: Error validating @default trait: String value provided for `smithy.example#Bar` must be one of the following values: `BAZ`, `FOO` | DefaultTrait diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/enum-must-match-values.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/enum-must-match-values.smithy new file mode 100644 index 00000000000..e51df037c5c --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/enum-must-match-values.smithy @@ -0,0 +1,13 @@ +$version: "2.0" + +namespace smithy.example + +structure Foo { + bar: Bar = "FOO" // valid + baz: Bar = "baz" // invalid +} + +enum Bar { + FOO + BAZ = "BAZ" +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/default-value-in-update/finds-patch-by-name-with-default.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/finds-patch-by-name-with-default.errors similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/default-value-in-update/finds-patch-by-name-with-default.errors rename to smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/finds-patch-by-name-with-default.errors diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/default-value-in-update/finds-patch-by-name-with-default.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/finds-patch-by-name-with-default.smithy similarity index 75% rename from smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/default-value-in-update/finds-patch-by-name-with-default.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/finds-patch-by-name-with-default.smithy index 3090742b563..e9f247e068f 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/default-value-in-update/finds-patch-by-name-with-default.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/finds-patch-by-name-with-default.smithy @@ -7,8 +7,7 @@ operation PatchFoo { @required id: String - @default - description: String + description: String = "" } output := {} } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/default-value-in-update/finds-resource-update-with-default.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/finds-resource-update-with-default.errors similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/default-value-in-update/finds-resource-update-with-default.errors rename to smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/finds-resource-update-with-default.errors diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/default-value-in-update/finds-resource-update-with-default.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/finds-resource-update-with-default.smithy similarity index 83% rename from smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/default-value-in-update/finds-resource-update-with-default.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/finds-resource-update-with-default.smithy index 1e9bb26db94..cde992dabd4 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/default-value-in-update/finds-resource-update-with-default.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/finds-resource-update-with-default.smithy @@ -14,8 +14,7 @@ operation UpdateFoo { @required id: String - @default - description: String + description: String = "" } output := {} } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/default-value-in-update/finds-update-by-name-with-default.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/finds-update-by-name-with-default.errors similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/default-value-in-update/finds-update-by-name-with-default.errors rename to smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/finds-update-by-name-with-default.errors diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/default-value-in-update/finds-update-by-name-with-default.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/finds-update-by-name-with-default.smithy similarity index 76% rename from smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/default-value-in-update/finds-update-by-name-with-default.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/finds-update-by-name-with-default.smithy index a8966cf0a09..1d0ad576e7f 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/default-value-in-update/finds-update-by-name-with-default.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/finds-update-by-name-with-default.smithy @@ -7,8 +7,7 @@ operation UpdateFoo { @required id: String - @default - description: String + description: String = "" } output := {} } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/invalid-type-for-string.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/invalid-type-for-string.errors new file mode 100644 index 00000000000..0bc04bbe205 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/invalid-type-for-string.errors @@ -0,0 +1 @@ +[ERROR] smithy.example#Foo$bar: Error validating @default trait: Expected string value for string shape, `smithy.api#String`; found number value, `10` | DefaultTrait diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/invalid-type-for-string.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/invalid-type-for-string.smithy new file mode 100644 index 00000000000..f1e40cdaa7c --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/invalid-type-for-string.smithy @@ -0,0 +1,7 @@ +$version: "2.0" + +namespace smithy.example + +structure Foo { + bar: String = 10 +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/list-must-be-empty.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/list-must-be-empty.errors new file mode 100644 index 00000000000..c9138b037bb --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/list-must-be-empty.errors @@ -0,0 +1 @@ +[ERROR] smithy.example#Foo$bar: The @default value of a list must be an empty list | DefaultTrait diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/list-must-be-empty.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/list-must-be-empty.smithy new file mode 100644 index 00000000000..462a50ad84a --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/list-must-be-empty.smithy @@ -0,0 +1,12 @@ +$version: "2.0" + +namespace smithy.example + +structure Foo { + @default(["foo"]) + bar: StringList +} + +list StringList { + member: String +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/list-must-match-constraints.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/list-must-match-constraints.errors new file mode 100644 index 00000000000..8dd8edc37cf --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/list-must-match-constraints.errors @@ -0,0 +1 @@ +[ERROR] smithy.example#Foo$bar: Error validating @default trait: Value provided for `smithy.example#StringList` must have at least 1 elements, but the provided value only has 0 elements | DefaultTrait diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/list-must-match-constraints.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/list-must-match-constraints.smithy new file mode 100644 index 00000000000..e4c779fabe2 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/list-must-match-constraints.smithy @@ -0,0 +1,12 @@ +$version: "2.0" + +namespace smithy.example + +structure Foo { + bar: StringList = [] +} + +@length(min: 1) +list StringList { + member: String +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/map-must-be-empty.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/map-must-be-empty.errors new file mode 100644 index 00000000000..a3e6aeed940 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/map-must-be-empty.errors @@ -0,0 +1 @@ +[ERROR] smithy.example#Foo$bar: The @default value of a map must be an empty map | DefaultTrait diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/map-must-be-empty.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/map-must-be-empty.smithy new file mode 100644 index 00000000000..32548de8fc2 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/map-must-be-empty.smithy @@ -0,0 +1,12 @@ +$version: "2.0" + +namespace smithy.example + +structure Foo { + bar: StringMap = {foo: "bar"} +} + +map StringMap { + key: String + value: String +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/map-must-match-constraints.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/map-must-match-constraints.errors new file mode 100644 index 00000000000..f521468a2d3 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/map-must-match-constraints.errors @@ -0,0 +1 @@ +[ERROR] smithy.example#Foo$bar: Error validating @default trait: Value provided for `smithy.example#StringMap` must have at least 1 entries, but the provided value only has 0 entries | DefaultTrait diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/map-must-match-constraints.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/map-must-match-constraints.smithy new file mode 100644 index 00000000000..2cd1206ab1d --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/map-must-match-constraints.smithy @@ -0,0 +1,13 @@ +$version: "2.0" + +namespace smithy.example + +structure Foo { + bar: StringMap = {} +} + +@length(min: 1) +map StringMap { + key: String + value: String +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/numbers-must-fit-type.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/numbers-must-fit-type.errors new file mode 100644 index 00000000000..73ba1ad8d12 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/numbers-must-fit-type.errors @@ -0,0 +1 @@ +[ERROR] smithy.example#Foo$badByte: Error validating @default trait: byte value must be < 127, but found 1024 | DefaultTrait diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/numbers-must-fit-type.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/numbers-must-fit-type.smithy new file mode 100644 index 00000000000..b4cb0b90fc7 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/numbers-must-fit-type.smithy @@ -0,0 +1,7 @@ +$version: "2.0" + +namespace smithy.example + +structure Foo { + badByte: Byte = 1024 +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/numbers-must-match-constraints.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/numbers-must-match-constraints.errors new file mode 100644 index 00000000000..d84ff8d3d50 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/numbers-must-match-constraints.errors @@ -0,0 +1 @@ +[ERROR] smithy.example#Foo$baz: Error validating @default trait: Value provided for `smithy.example#Baz` must be greater than or equal to 1, but found 0 | DefaultTrait diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/numbers-must-match-constraints.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/numbers-must-match-constraints.smithy new file mode 100644 index 00000000000..12675693613 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/numbers-must-match-constraints.smithy @@ -0,0 +1,11 @@ +$version: "2.0" + +namespace smithy.example + +structure Foo { + bar: Baz = 1 + baz: Baz = 0 +} + +@range(min: 1, max: 10) +integer Baz diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/string-must-match-constraints.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/string-must-match-constraints.errors new file mode 100644 index 00000000000..e8bb4d4915d --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/string-must-match-constraints.errors @@ -0,0 +1,2 @@ +[ERROR] smithy.example#Foo$bar: Error validating @default trait: String value provided for `smithy.example#Bar` must be >= 5 characters, but the provided value is only 2 characters. | DefaultTrait +[ERROR] smithy.example#Foo$baz: Error validating @default trait: String value provided for `smithy.example#Baz` must match regular expression: ^[A-Z]+$ | DefaultTrait diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/string-must-match-constraints.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/string-must-match-constraints.smithy new file mode 100644 index 00000000000..12dbc89ab53 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/string-must-match-constraints.smithy @@ -0,0 +1,14 @@ +$version: "2.0" + +namespace smithy.example + +structure Foo { + bar: Bar = "hi" + baz: Baz = "bye" +} + +@length(min: 5) +string Bar + +@pattern("^[A-Z]+$") +string Baz diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors index 29cf8916b9b..3d0bdb65dc9 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors @@ -7,7 +7,4 @@ [WARNING] ns.foo#IntEnum$undesirableName: The name `undesirableName` does not match the recommended enum name format of beginning with an uppercase letter, followed by any number of uppercase letters, numbers, or underscores. | EnumShape [WARNING] ns.foo#EnumWithEnumTrait: This shape applies a trait that is deprecated: smithy.api#enum | DeprecatedTrait [ERROR] ns.foo#EnumWithEnumTrait: Trait `enum` cannot be applied to `ns.foo#EnumWithEnumTrait`. This trait may only be applied to shapes that match the following selector: string :not(enum) | TraitTarget -[ERROR] ns.foo#StringEnum$EMPTY_STRING: enum values may not be empty because an empty string is the default value of enum shapes. Instead, use `smithy.api#enumDefault` to set an explicit name for the default value. | EnumShape -[ERROR] ns.foo#MultipleDefaults: The `enumDefault` trait can be applied to only a single member of a shape, but it was found on the following members: `DEFAULT1`, `DEFAULT2` | ExclusiveStructureMemberTrait -[ERROR] ns.foo#DefaultWithExplicitValue$DEFAULT: Found conflicting traits on member shape: `enumDefault` conflicts with `enumValue` | TraitConflict -[ERROR] ns.foo#IntEnum$ZERO: intEnum values may not be set to 0 because 0 is the default value of intEnum shapes. Instead, use `smithy.api#enumDefault` to set an explicit name for the default value. | EnumShape +[ERROR] ns.foo#StringEnum$EMPTY_STRING: enum values may not be empty. | EnumShape diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy index c771b0cf1c7..35fb58c35ca 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy @@ -11,9 +11,6 @@ enum StringEnum { @enumValue("") EMPTY_STRING - @enumDefault - DEFAULT_VALUE - @enumValue(1) INT_VALUE @@ -31,20 +28,6 @@ enum EnumWithEnumTrait { BAR } -enum MultipleDefaults { - @enumDefault - DEFAULT1 - - @enumDefault - DEFAULT2 -} - -enum DefaultWithExplicitValue { - @enumDefault - @enumValue("foo") - DEFAULT -} - intEnum IntEnum { IMPLICIT_VALUE @@ -54,9 +37,6 @@ intEnum IntEnum { @enumValue(0) ZERO - @enumDefault - DEFAULT_VALUE - @enumValue("foo") STRING_VALUE diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/requiresLength-validator.json b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/requiresLength-validator.json index 3c5af052e2e..a8d328ea865 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/requiresLength-validator.json +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/requiresLength-validator.json @@ -19,7 +19,7 @@ "Body": { "target": "ns.foo#StreamingBlob", "traits": { - "smithy.api#default": {} + "smithy.api#default": "" } } }, @@ -33,7 +33,7 @@ "Body": { "target": "ns.foo#StreamingBlob", "traits": { - "smithy.api#default": {} + "smithy.api#default": "" } } }, diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/streaming-blob-without-httppayload.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/streaming-blob-without-httppayload.smithy index 485ef1cba42..bb1164e1aa6 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/streaming-blob-without-httppayload.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/streaming-blob-without-httppayload.smithy @@ -30,7 +30,7 @@ structure StreamingOperationOutput { @required streamId: String, - @default + @default("") output: StreamingBlob, } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/streaming-default-required.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/streaming-default-required.smithy index b4b35c2ba6d..388a9790f56 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/streaming-default-required.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/streaming-default-required.smithy @@ -18,7 +18,7 @@ operation DefaultStreamOperation { } structure DefaultStream { - @default + @default("") payload: StreamingBlob } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/streaming-trait.json b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/streaming-trait.json index 1bf6a88afff..063afc0c47c 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/streaming-trait.json +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/streaming-trait.json @@ -19,7 +19,7 @@ "Body": { "target": "ns.foo#StreamingBlob", "traits": { - "smithy.api#default": {} + "smithy.api#default": "" } } }, @@ -33,7 +33,7 @@ "Body": { "target": "ns.foo#StreamingBlob", "traits": { - "smithy.api#default": {} + "smithy.api#default": "" } } }, @@ -71,7 +71,7 @@ "Body": { "target": "ns.foo#StreamingBlob", "traits": { - "smithy.api#default": {} + "smithy.api#default": "" } } }, @@ -100,13 +100,13 @@ "StreamingBlob1": { "target": "ns.foo#StreamingBlob", "traits": { - "smithy.api#default": {} + "smithy.api#default": "" } }, "StreamingBlob2": { "target": "ns.foo#StreamingBlob", "traits": { - "smithy.api#default": {} + "smithy.api#default": "" } } }, @@ -144,7 +144,7 @@ "nested": { "target": "ns.foo#StreamingBlob", "traits": { - "smithy.api#default": {} + "smithy.api#default": "" } } } @@ -163,7 +163,7 @@ "foo": { "target": "ns.foo#StreamingBlob", "traits": { - "smithy.api#default": {} + "smithy.api#default": "" } } }, diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-incomplete-node.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-incomplete-node.smithy new file mode 100644 index 00000000000..687b21ba109 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-incomplete-node.smithy @@ -0,0 +1,7 @@ +// Parse error at line 8, column 1 near ``: Expected: ']' +$version: "2.0" + +namespace com.foo + +structure Foo { + bar: String = [ diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-missing-structure-close.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-missing-structure-close.smithy new file mode 100644 index 00000000000..d7b195a5bc3 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-missing-structure-close.smithy @@ -0,0 +1,7 @@ +// Parse error at line 8, column 1 near ``: Expected: '}' +$version: "2.0" + +namespace com.foo + +structure Foo { + bar: String = "" diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-missing-value.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-missing-value.smithy new file mode 100644 index 00000000000..85a16186ac6 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-missing-value.smithy @@ -0,0 +1,8 @@ +// Parse error at line 8, column 1 near `}\n`: Expected a valid identifier character, but found '}' +$version: "2.0" + +namespace com.foo + +structure Foo { + bar: String = +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/unions-do-not-support-defaults.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/unions-do-not-support-defaults.smithy new file mode 100644 index 00000000000..34487a654f7 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/unions-do-not-support-defaults.smithy @@ -0,0 +1,8 @@ +// Parse error at line 7, column 17 near `= ` +$version: "2.0" + +namespace com.foo + +union Foo { + bar: String = "hi" +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/enum-with-bad-values.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/enum-with-bad-values.smithy new file mode 100644 index 00000000000..4e7a1dcb7ee --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/enum-with-bad-values.smithy @@ -0,0 +1,8 @@ +// smithy.example#Foo$BAR: enum shapes require string values but found: 10 +$version: "2.0" + +namespace smithy.example + +enum Foo { + BAR = 10 +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/intEnum-with-bad-value.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/intEnum-with-bad-value.smithy new file mode 100644 index 00000000000..6e053230f40 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/intEnum-with-bad-value.smithy @@ -0,0 +1,8 @@ +// [ERROR] smithy.example#Foo$BAR: intEnum shapes require integer values but found: "Abc" +$version: "2.0" + +namespace smithy.example + +intEnum Foo { + BAR = "Abc" +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/intEnum-with-float.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/intEnum-with-float.smithy new file mode 100644 index 00000000000..0b3822c3ab9 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/intEnum-with-float.smithy @@ -0,0 +1,8 @@ +// [ERROR] smithy.example#Foo$BAR: intEnum shapes do not support floating point values +$version: "2.0" + +namespace smithy.example + +intEnum Foo { + BAR = 1.5 +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/inline-io/inline-structure-member.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/inline-io/inline-structure-member.smithy index 7f83ad69c79..6d968beb4ec 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/inline-io/inline-structure-member.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/inline-io/inline-structure-member.smithy @@ -1,4 +1,4 @@ -// Defining structures inline with the `:=` syntax may only be used when defining operation input and output shapes. +// Parse error at line 6, column 14 near `= `: Expected a valid identifier character, but found '=' $version: "2.0" namespace smithy.example diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/all-1.0/upgraded.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/all-1.0/upgraded.smithy index 9f2a3894e54..8f5e52bcf3e 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/all-1.0/upgraded.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/all-1.0/upgraded.smithy @@ -5,7 +5,7 @@ namespace smithy.example structure Bytes { nullable: Byte, - @default + @default(0) nonNull: Byte, nullable2: Byte, @@ -14,7 +14,7 @@ structure Bytes { structure Shorts { nullable: Short, - @default + @default(0) nonNull: Short, nullable2: Short, @@ -23,14 +23,14 @@ structure Shorts { structure Integers { nullable: Integer, - @default + @default(0) nonNull: Integer, nullable2: Integer } structure BlobPayload { - @default + @default("") payload: StreamingBlob } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/main.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/main.smithy index 2dff954d919..90392a314b0 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/main.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/main.smithy @@ -3,7 +3,7 @@ $version: "1.0" namespace smithy.example structure Foo { - @default + @default(0) alreadyDefault: PrimitiveInteger, @required diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/upgraded.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/upgraded.smithy index 1e8dddd930e..99126e22512 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/upgraded.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/upgraded.smithy @@ -3,7 +3,7 @@ $version: "2.0" namespace smithy.example structure Foo { - @default + @default(0) alreadyDefault: Integer, @required diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/mixed-versions/2.0.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/mixed-versions/2.0.smithy index fa5a8a8619e..0d69262bd9d 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/mixed-versions/2.0.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/mixed-versions/2.0.smithy @@ -3,6 +3,6 @@ $version: "2.0" namespace smithy.example structure Baz { - @default + @default(0) number: Integer } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/mixed-versions/upgraded.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/mixed-versions/upgraded.smithy index 41704379f1e..e261b8be24e 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/mixed-versions/upgraded.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/mixed-versions/upgraded.smithy @@ -3,11 +3,11 @@ $version: "2.0" namespace smithy.example structure Foo { - @default + @default(0) number: Integer } structure Baz { - @default + @default(0) number: Integer } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/defaults/valid-defaults.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/defaults/valid-defaults.json new file mode 100644 index 00000000000..78594e2a20c --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/defaults/valid-defaults.json @@ -0,0 +1,139 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.example#Foo": { + "type": "structure", + "members": { + "a": { + "target": "smithy.api#String", + "traits": { + "smithy.api#default": "" + } + }, + "b": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#default": true + } + }, + "c": { + "target": "smithy.example#StringList", + "traits": { + "smithy.api#default": [] + } + }, + "d": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#default": {} + } + }, + "e": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#default": "hi" + } + }, + "f": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#default": true + } + }, + "g": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#default": false + } + }, + "h": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#default": [] + } + }, + "i": { + "target": "smithy.api#Timestamp", + "traits": { + "smithy.api#default": 0 + } + }, + "j": { + "target": "smithy.api#Blob", + "traits": { + "smithy.api#default": "" + } + }, + "k": { + "target": "smithy.api#Byte", + "traits": { + "smithy.api#default": 1 + } + }, + "l": { + "target": "smithy.api#Short", + "traits": { + "smithy.api#default": 1 + } + }, + "m": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#default": 10 + } + }, + "n": { + "target": "smithy.api#Long", + "traits": { + "smithy.api#default": 100 + } + }, + "o": { + "target": "smithy.api#Float", + "traits": { + "smithy.api#default": 0 + } + }, + "p": { + "target": "smithy.api#Double", + "traits": { + "smithy.api#default": 0 + } + }, + "q": { + "target": "smithy.example#StringMap", + "traits": { + "smithy.api#default": {} + } + }, + "r": { + "target": "smithy.api#BigInteger", + "traits": { + "smithy.api#default": 0 + } + }, + "s": { + "target": "smithy.api#BigDecimal", + "traits": { + "smithy.api#default": 0 + } + } + } + }, + "smithy.example#StringList": { + "type": "list", + "member": { + "target": "smithy.api#String" + } + }, + "smithy.example#StringMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.api#String" + } + } + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/defaults/valid-defaults.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/defaults/valid-defaults.smithy new file mode 100644 index 00000000000..55ee02e295e --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/defaults/valid-defaults.smithy @@ -0,0 +1,35 @@ +$version: "2.0" + +namespace smithy.example + +structure Foo { + a: String = "" + b: Boolean = true + c: StringList = [] + d: Document = {} + e: Document = "hi" + f: Document = true + g: Document = false + h: Document = [] + i: Timestamp = 0 + j: Blob = "" + k: Byte = 1 + l: Short = 1 + m: Integer = 10 + n: Long = 100 + o: Float = 0 + p: Double= 0 + q: StringMap = + {} + r: BigInteger = 0 + s: BigDecimal = 0 +} + +list StringList { + member: String +} + +map StringMap { + key: String + value: String +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.json index 9ac47eaf261..b7abfbeaa97 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.json +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.json @@ -47,17 +47,6 @@ } } }, - "smithy.example#EnumWithDefaultBound": { - "type": "enum", - "members": { - "DEFAULT": { - "target": "smithy.api#Unit", - "traits": { - "smithy.api#enumDefault": {} - } - } - } - }, "smithy.example#IntEnum": { "type": "intEnum", "members": { @@ -80,17 +69,6 @@ } } } - }, - "smithy.example#IntEnumWithDefaultBound": { - "type": "intEnum", - "members": { - "DEFAULT": { - "target": "smithy.api#Unit", - "traits": { - "smithy.api#enumDefault": {} - } - } - } } } } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.smithy index 38e6ef50ac1..36b9b62a7bd 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.smithy @@ -19,11 +19,6 @@ enum EnumWithValueTraits { BAZ } -enum EnumWithDefaultBound { - @enumDefault - DEFAULT -} - intEnum IntEnum { @enumValue(1) FOO @@ -34,8 +29,3 @@ intEnum IntEnum { @enumValue(3) BAZ } - -intEnum IntEnumWithDefaultBound { - @enumDefault - DEFAULT -} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/forward-reference-resolver.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/forward-reference-resolver.json index 9eabb4b3fc3..54684d16344 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/forward-reference-resolver.json +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/forward-reference-resolver.json @@ -1,5 +1,5 @@ { - "smithy": "1.0", + "smithy": "2.0", "shapes": { "smithy.example#Foo": { "type": "structure", diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/forward-reference-resolver.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/forward-reference-resolver.smithy index 39be229d58b..f61da5de16d 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/forward-reference-resolver.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/forward-reference-resolver.smithy @@ -1,3 +1,5 @@ +$version: "2.0" + namespace smithy.example structure Foo { diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/default-traits.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/default-traits.smithy new file mode 100644 index 00000000000..0489fa257dd --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/default-traits.smithy @@ -0,0 +1,8 @@ +$version: "2.0" + +namespace ns.foo + +structure Foo { + bar: String = "" + baz: String = "hi" +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/enums.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/enums.smithy index b49611fb907..b4c53ec5ad1 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/enums.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/enums.smithy @@ -3,10 +3,8 @@ $version: "2.0" namespace ns.foo intEnum IntEnum { - @enumValue(1) - FOO - @enumValue(2) - BAR + FOO = 1 + BAR = 2 } enum StringEnum { @@ -15,8 +13,6 @@ enum StringEnum { } enum StringEnumWithExplicitValues { - @enumValue("foo") - FOO - @enumValue("bar") - BAR + FOO = "foo" + BAR = "bar" } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/validation/node-validator.json b/smithy-model/src/test/resources/software/amazon/smithy/model/validation/node-validator.json index 7bf9b9304c6..10339b85d78 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/validation/node-validator.json +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/validation/node-validator.json @@ -269,7 +269,7 @@ "defaultedInt": { "target": "smithy.api#Integer", "traits": { - "smithy.api#default": {} + "smithy.api#default": 0 } }, "requiredInt": { diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/streaming-service.openapi.json b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/streaming-service.openapi.json index 8a2af81b54f..2cf70d4d221 100644 --- a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/streaming-service.openapi.json +++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/streaming-service.openapi.json @@ -27,7 +27,8 @@ "schemas": { "StreamingOperationOutputPayload": { "type": "string", - "format": "byte" + "format": "byte", + "default": "" } } } diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/streaming-service.smithy b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/streaming-service.smithy index 44eb96d3449..56a22009238 100644 --- a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/streaming-service.smithy +++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/streaming-service.smithy @@ -15,9 +15,8 @@ operation StreamingOperation { } structure Output { - @default @httpPayload - body: StreamingPayload, + body: StreamingPayload = "" } @streaming diff --git a/smithy-waiters/src/main/resources/META-INF/smithy/waiters.smithy b/smithy-waiters/src/main/resources/META-INF/smithy/waiters.smithy index 97ade1f0481..ab2f354bb32 100644 --- a/smithy-waiters/src/main/resources/META-INF/smithy/waiters.smithy +++ b/smithy-waiters/src/main/resources/META-INF/smithy/waiters.smithy @@ -28,12 +28,12 @@ structure Waiter { /// This value defaults to 2 if not specified. If specified, this value /// MUST be greater than or equal to 1 and less than or equal to /// `maxDelay`. - minDelay: WaiterDelay, + minDelay: WaiterDelay = 2, /// The maximum amount of time in seconds to delay between each retry. /// This value defaults to 120 if not specified (or, 2 minutes). If /// specified, this value MUST be greater than or equal to 1. - maxDelay: WaiterDelay, + maxDelay: WaiterDelay = 120, /// Indicates if the waiter is considered deprecated. A waiter SHOULD /// be marked as deprecated if it has been replaced by another waiter or @@ -72,18 +72,15 @@ structure Acceptor { enum AcceptorState { /// The waiter successfully finished waiting. This is a terminal /// state that causes the waiter to stop. - @enumValue("success") - SUCCESS + SUCCESS = "success" /// The waiter failed to enter into the desired state. This is a /// terminal state that causes the waiter to stop. - @enumValue("failure") - FAILURE + FAILURE = "failure" /// The waiter will retry the operation. This state transition is /// implicit if no accepter causes a state transition. - @enumValue("retry") - RETRY + RETRY = "retry" } /// Defines how an acceptor determines if it matches the current state of @@ -136,20 +133,16 @@ structure PathMatcher { @private enum PathComparator { /// Matches if the return value is a string that is equal to the expected string. - @enumValue("stringEquals") - STRING_EQUALS + STRING_EQUALS = "stringEquals" /// Matches if the return value is a boolean that is equal to the string literal 'true' or 'false'. - @enumValue("booleanEquals") - BOOLEAN_EQUALS + BOOLEAN_EQUALS = "booleanEquals" /// Matches if all values in the list matches the expected string. - @enumValue("allStringEquals") - ALL_STRING_EQUALS + ALL_STRING_EQUALS = "allStringEquals" /// Matches if any value in the list matches the expected string. - @enumValue("anyStringEquals") - ANY_STRING_EQUALS + ANY_STRING_EQUALS = "anyStringEquals" } @private