|
| 1 | +# SOAR-0002: Improved naming of content types |
| 2 | + |
| 3 | +Improved naming of content types to Swift identifiers. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +- Proposal: SOAR-0002 |
| 8 | +- Author(s): [Honza Dvorsky](https://github.com/czechboy0) |
| 9 | +- Status: **In Preview** |
| 10 | +- Issue: N/A, was part of multiple content type support: [apple/swift-openapi-generator#6](https://github.com/apple/swift-openapi-generator/issues/6) and [apple/swift-openapi-generator#7](https://github.com/apple/swift-openapi-generator/issues/7) |
| 11 | +- Implementation: |
| 12 | + - [Landed behind a feature flag as part of apple/swift-openapi-generator#146](https://github.com/czechboy0/swift-openapi-generator/blob/4555f8e998b24aa65a462a63828d9195c50dcc23/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentSwiftName.swift#L23-L42) |
| 13 | +- Feature flag: `multipleContentTypes` |
| 14 | +- Affected components: |
| 15 | + - generator |
| 16 | +- Versions: |
| 17 | + - v1 (2023-08-07): First draft |
| 18 | + - v2 (2023-08-08): Second draft with the following changes: |
| 19 | + - added 6 more short names |
| 20 | + - updated short names for a few of the originally proposed content types |
| 21 | + - updated the logic for generic names, gets rid of `_sol_` for the slash |
| 22 | + - v3 (2023-08-08): Third draft with the following changes: |
| 23 | + - `multipart/form-data` short name changed from `formData` to `multipartForm` |
| 24 | + |
| 25 | +### Introduction |
| 26 | + |
| 27 | +Introduce a new content type -> Swift name naming scheme to allow for multiple content types within the same request or response body. |
| 28 | + |
| 29 | +### Motivation |
| 30 | + |
| 31 | +Previously, the logic for assigning a Swift name to a content type always produced one of the following three strings: `json`, `text`, or `binary`. |
| 32 | + |
| 33 | +That worked fine at the beginning, but now with multiple content type support for [request](https://github.com/apple/swift-openapi-generator/issues/7) and [response](https://github.com/apple/swift-openapi-generator/issues/6) bodies landed behind a feature flag, we need a naming scheme that produces much fewer conflicts. |
| 34 | + |
| 35 | +Without the change, the following OpenAPI snippet would continue to fail to build: |
| 36 | + |
| 37 | +```yaml |
| 38 | +paths: |
| 39 | + /foo: |
| 40 | + get: |
| 41 | + responses: |
| 42 | + '200': |
| 43 | + content: |
| 44 | + application/json: {} |
| 45 | + application/vendor1+json: {} |
| 46 | + application/vendor2+json: {} |
| 47 | +``` |
| 48 | +
|
| 49 | +That's because all three would use the name `json` in the generated `Output.*.Body` enum. |
| 50 | + |
| 51 | +There are currently no workarounds apart from removing the additional content types from your OpenAPI document. |
| 52 | + |
| 53 | +### Proposed solution |
| 54 | + |
| 55 | +I propose to extend the naming logic to achieve two goals: |
| 56 | +- continue to use short and ergonomic names for common content types, like today |
| 57 | +- avoid conflicts for arbitrary, less common content types using the new logic introduced in [SOAR-0001](https://github.com/apple/swift-openapi-generator/blob/main/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0001.md) _for each component of the content type, and concatenate them with an underscore_ (**changed in v2**) |
| 58 | + |
| 59 | +In practical terms, it means that if a content type exactly matches one of the predefined content types that have a short name assigned, the short name will be used. |
| 60 | + |
| 61 | +Otherwise, each component of the content type string (for an example `application/vendor1+json` the components would be `application` and `vendor1+json`) will be passed to the `swiftSafeName` function, which was improved in [SOAR-0001](https://github.com/apple/swift-openapi-generator/blob/main/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0001.md), and produce a deterministic name that is unlikely to conflict with any other content type. |
| 62 | + |
| 63 | +Let's look at a few examples: |
| 64 | +- for a common content type, such as `application/json`, a short name `json` will be used |
| 65 | +- for an arbitrary content type, such as `application/vendor1+json`, a deterministic name will be produced, such as `application_vendor1_plus_json` (**changed in v2**, was `application_sol_vendor1_plus_json` in v1) |
| 66 | + |
| 67 | +This way, adopters continue to get short names for commonly used content types, but can also use completely custom content types, without getting a build error in the generated code. |
| 68 | + |
| 69 | +### Detailed design |
| 70 | + |
| 71 | +The whole implementation of the proposed logic for the function `func contentSwiftName(_ contentType: ContentType) -> String` in `FileTranslator` would change to the following (shows the list of predefined content types): |
| 72 | + |
| 73 | +```swift |
| 74 | +func contentSwiftName(_ contentType: ContentType) -> String { |
| 75 | + switch contentType.lowercasedTypeAndSubtype { |
| 76 | + case "application/json": |
| 77 | + return "json" |
| 78 | + case "application/x-www-form-urlencoded": |
| 79 | + return "urlEncodedForm" |
| 80 | + case "multipart/form-data": |
| 81 | + return "multipartForm" |
| 82 | + case "text/plain": |
| 83 | + return "plainText" |
| 84 | + case "*/*": |
| 85 | + return "any" |
| 86 | + case "application/xml": |
| 87 | + return "xml" |
| 88 | + case "application/octet-stream": |
| 89 | + return "binary" |
| 90 | + case "text/html": |
| 91 | + return "html" |
| 92 | + case "application/yaml": |
| 93 | + return "yaml" |
| 94 | + case "text/csv": |
| 95 | + return "csv" |
| 96 | + case "image/png": |
| 97 | + return "png" |
| 98 | + case "application/pdf": |
| 99 | + return "pdf" |
| 100 | + case "image/jpeg": |
| 101 | + return "jpeg" |
| 102 | + default: |
| 103 | + let safedType = swiftSafeName(for: contentType.originallyCasedType) |
| 104 | + let safedSubtype = swiftSafeName(for: contentType.originallyCasedSubtype) |
| 105 | + return "\(safedType)_\(safedSubtype)" |
| 106 | + } |
| 107 | +} |
| 108 | +``` |
| 109 | + |
| 110 | +The above shows that the content types that have a short name assigned are: |
| 111 | +- `application/json` -> `json` |
| 112 | +- `application/x-www-form-urlencoded` -> `urlEncodedForm` (**changed in v2**, was `form` in v1) |
| 113 | +- `multipart/form-data` -> `multipartForm` (**changed in v2 and v3**, was `multipart` in v1, `formData` in v2) |
| 114 | +- `text/plain` -> `plainText` (**changed in v2**, was `text` in v1) |
| 115 | +- `*/*` -> `any` |
| 116 | +- `application/xml` -> `xml` |
| 117 | +- `application/octet-stream` -> `binary` |
| 118 | +- `text/html` -> `html` (**added in v2**) |
| 119 | +- `application/yaml` -> `yaml` (**added in v2**) |
| 120 | +- `text/csv` -> `csv` (**added in v2**) |
| 121 | +- `image/png` -> `png` (**added in v2**) |
| 122 | +- `application/pdf` -> `pdf` (**added in v2**) |
| 123 | +- `image/jpeg` -> `jpeg` (**added in v2**) |
| 124 | + |
| 125 | +These specific values were not chosen arbitrarily, instead I wrote a script that collected and processed about 1200 OpenAPI documents from the wild, and aggregated usage statistics. These content types, in this order, were the top used content types from those documents. |
| 126 | + |
| 127 | +> Note: While Swift OpenAPI Generator does not yet support some of the content types above (such as `multipart/form-data` (tracked by [#36](https://github.com/apple/swift-openapi-generator/issues/36)) and `*/*` (tracked by [#71](https://github.com/apple/swift-openapi-generator/issues/71))), we should still make room for them here now, as changing the naming logic is a breaking change, so we don't want to undergo it again in the future. |
| 128 | + |
| 129 | +### API stability |
| 130 | + |
| 131 | +This change breaks backwards compatibility of existing generated code as it renames the enum cases in the generated `Body` enums for requests and responses. |
| 132 | + |
| 133 | +The change is currently hidden behind the `multipleContentTypes` feature flag, and once approved, would be rolled out together with that feature in the next breaking version (likely 0.2.0). |
| 134 | + |
| 135 | +No other API impact. |
| 136 | + |
| 137 | +### Future directions |
| 138 | + |
| 139 | +Nothing comes to mind right now, as we already make provisions for not-yet-supported content types (see the note about `multipart/form-data` and `*/*`), so I'm not expecting a need to change this naming logic again. |
| 140 | + |
| 141 | +### Alternatives considered |
| 142 | + |
| 143 | +#### No short names |
| 144 | + |
| 145 | +A conceptually simpler solution to the problem of conflicting content type Swift names was to always generate full names (such as `application/vendor1+json` -> `application_vendor1_plus_json`), however that would have resulted in unnecessarily long names for common content types, for example, `application/json` would have been `application_json`, instead of `json`. _However, projects in the ecosystem that provide type-safe access to common content types also use short names, showing that developers don't seem to get confused by the commonly used short names._ (**sentence added in v2**) |
| 146 | + |
| 147 | +This idea was rejected as data from real-world OpenAPI documents showed that there is a very small number (~13) (**changed in v2**, was ~7 in v1) of content types that are used most often, so making the readability for adopters easier comes at a relatively low cost (see the full implementation of the naming logic above). This follows the principle of making the simple things easy/pretty, and difficult things possible/usable. |
0 commit comments