Repository: CreateAPI/CreateAPI (swift)

Issue:
Support custom mappings between schema formats and Swift types
Edit: differs from the original to a single global override instead of individual properties and I forgot parameter types. I think that we can start with a global override and somebody can request this fine tuning later.

---

Introduce a config option to override the Swift type when they are parsed and written:

```yaml
# schema
Pet:
  properties:
    id:
      type: string
    tag:
      type: string
      format: uuid

# config
swiftTypeOverrides:
  UUID: String # any occurrences of UUID are replaced with String, so `Pet.tag: String`
```

Config name open for improvement. Obviously, a developer might override to a type that cannot be decoded from/encoded to the original type, like `UUID: Int` or a type that does not exist, like `UUID: qwerty`. So, it is a developer responsibility to make sure that overriding types can be properly decoded/encoded and that they are a proper Swift type.

---

This can be further enhanced with imports as we should be trivially able to set an imported type after https://github.com/CreateAPI/CreateAPI/issues/85 is figured out. Using NaiveDate as an example removing the `useNaiveDate` option, we include the package as an import in the config and then set the type override:

```yaml
# schema
Pet:
  properties:
    birth_date:
      type: string
      format: date

# config
swiftTypeOverrides:
  Date: NaiveDate # any occurrences of Date are replaced with NaiveDate
```

Obviously, imported objects should be expected to properly decode/encode but that's an implementation detail of the imported type.

Pull request:
- Closes https://github.com/CreateAPI/CreateAPI/issues/114

This PR allows custom Swift type overrides for schema datatype formats (not global for this):

```yaml
datatypes:
  string:
    uuid: String
    password: MyPasswordType
  integer:
    int32: Int
    int64: Int
```

Also removes `useFixWidthIntegers` in favor of the above example datatype override.

Patch:
diff --git a/Docs/ConfigOptions.md b/Docs/ConfigOptions.md
index f9855d97..2a63bef9 100644
--- a/Docs/ConfigOptions.md
+++ b/Docs/ConfigOptions.md
@@ -56,9 +56,9 @@ Below you can find the complete documentation for all available options.
 - [spaceWidth](#spacewidth)
 - [pluralizeProperties](#pluralizeproperties)
 - [useNaiveDate](#usenaivedate)
-- [useFixWidthIntegers](#usefixwidthintegers)
 - [fileHeaderComment](#fileheadercomment)
 - [commentOptions](#commentoptions)
+- [dataTypes](#datatypes)
 - [package](#package)
   - [dependencies](#packagedependencies)
 - [entities](#entities)
@@ -282,15 +282,6 @@ Parses dates (e.g. `"2021-09-29"`) using [NaiveDate](https://github.com/CreateAP
 
 <br/>
 
-## useFixWidthIntegers
-
-**Type:** Bool<br />
-**Default:** `false`
-
-If enabled, uses `Int64` or `Int32` when specified.
-
-<br/>
-
 ## fileHeaderComment
 
 **Type:** String<br />
@@ -349,6 +340,35 @@ commentOptions:
 
 <br/>
 
+## dataTypes
+
+**Type:** DataTypes<br />
+**Default:** `DataTypes()`
+
+Change datatype format mapping to Swift types than what CreateAPI provides. Use in combination with [`entity imports`](#entitiesimports), [`paths imports`](#pathsimports), and [`dependencies`](#packagedependencies) for mapping to types that the default library does not provide.
+
+> **Note** `useNaiveDate` takes precedence for overriding the `date` string format. Set `useNaiveDate: false` to properly override this format.
+
+> **Note** Swift types must conform to `Codable`. Incorrect decoding may cause crashes on API calls and incorrect encoding may cause API issues.
+
+<details>
+<summary>Examples</summary>
+
+```yaml
+datatypes:
+  string:
+    uuid: String
+    phone-number: PhoneNumber # imported type
+  number:
+    float: Double
+  integer:
+    int32: Int
+    int64: Int
+```
+</details>
+
+<br/>
+
 
 # Entities
 
diff --git a/Sources/CreateAPI/Generator/Generator+DataTypes.swift b/Sources/CreateAPI/Generator/Generator+DataTypes.swift
new file mode 100644
index 00000000..6c9496ff
--- /dev/null
+++ b/Sources/CreateAPI/Generator/Generator+DataTypes.swift
@@ -0,0 +1,62 @@
+import Foundation
+import OpenAPIKit30
+
+// TODO: Uncomment sections here for proper overrride behavior and update tests
+
+extension Generator {
+    
+    func stringType(for format: JSONTypeFormat.StringFormat) -> TypeIdentifier {
+        switch format {
+        case .byte:
+            return builtInType("Data", format: "byte", overrides: options.dataTypes.string)
+        case .date:
+            if options.useNaiveDate {
+                setNaiveDateNeeded()
+                return .builtin("NaiveDate")
+            }
+            return builtInType("String", format: "date", overrides: options.dataTypes.string)
+        case .dateTime:
+            return builtInType("Date", format: "date-time", overrides: options.dataTypes.string)
+        case .other("uri"):
+            return builtInType("URL", format: "uri", overrides: options.dataTypes.string)
+        case .other("uuid"):
+            return builtInType("UUID", format: "uuid", overrides: options.dataTypes.string)
+        case .other(let format):
+            return builtInType("String", format: format, overrides: options.dataTypes.string)
+        default:
+            return .builtin("String")
+        }
+    }
+    
+    func numberType(for format: JSONTypeFormat.NumberFormat) -> TypeIdentifier {
+        switch format {
+//        case .double:
+//            return builtInType("Double", format: "double", overrides: options.dataTypes.number)
+//        case .float:
+//            return builtInType("Float", format: "float", overrides: options.dataTypes.number)
+//        case .other(let format):
+//            return builtInType("Double", format: format, overrides: options.dataTypes.number)
+        default:
+            return .builtin("Double")
+        }
+    }
+    
+    func integerType(for format: JSONTypeFormat.IntegerFormat) -> TypeIdentifier {
+        switch format {
+        case .int32:
+            return builtInType("Int", format: "int32", overrides: options.dataTypes.integer)
+//            return builtInType("Int32", format: "int32", overrides: options.dataTypes.integer)
+        case .int64:
+            return builtInType("Int", format: "int64", overrides: options.dataTypes.integer)
+//            return builtInType("Int64", format: "int64", overrides: options.dataTypes.integer)
+        case .other(let format):
+            return builtInType("Int", format: format, overrides: options.dataTypes.integer)
+        default:
+            return .builtin("Int")
+        }
+    }
+    
+    private func builtInType(_ original: String, format: String, overrides: [String: String]?) -> TypeIdentifier {
+        .builtin(overrides?[format] ?? original)
+    }
+}
diff --git a/Sources/CreateAPI/Generator/Generator+Paths.swift b/Sources/CreateAPI/Generator/Generator+Paths.swift
index baf6195c..4b830d7c 100644
--- a/Sources/CreateAPI/Generator/Generator+Paths.swift
+++ b/Sources/CreateAPI/Generator/Generator+Paths.swift
@@ -530,32 +530,18 @@ extension Generator {
         func getQueryItemType(for schema: JSONSchema, isTopLevel: Bool) throws -> QueryItemType? {
             switch schema.value {
             case .boolean: return QueryItemType("Bool")
-            case .number: return QueryItemType("Double")
+            case .number(let info, _):
+                return QueryItemType(type: numberType(for: info.format))
             case .integer(let info, _):
-                guard options.useFixWidthIntegers else {
-                    return QueryItemType("Int")
-                }
-                switch info.format {
-                case .generic, .other: return QueryItemType("Int")
-                case .int32: return QueryItemType("Int32")
-                case .int64: return QueryItemType("Int64")
-                }
+                return QueryItemType(type: integerType(for: info.format))
             case .string(let info, _):
-                switch info.format {
-                case .dateTime: return QueryItemType("Date")
-                case .date: if options.useNaiveDate {
-                    setNaiveDateNeeded()
-                    return QueryItemType("NaiveDate")
-                }
-                case .other(let other): if other == "uri" { return QueryItemType("URL") }
-                default: break
-                }
                 if info.allowedValues != nil {
                     let enumTypeName = makeTypeName(parameter.name)
                     let nested = try makeStringEnum(name: enumTypeName, info: info)
                     return QueryItemType(type: .userDefined(name: enumTypeName), nested: nested)
                 }
-                return QueryItemType("String")
+                
+                return QueryItemType(type: stringType(for: info.format))
             case .object, .all, .one, .any:
                 let type = makeTypeName(parameter.name)
                 let nested = try _makeDeclaration(name: type, schema: schema, context: context)
diff --git a/Sources/CreateAPI/Generator/Generator+Schemas.swift b/Sources/CreateAPI/Generator/Generator+Schemas.swift
index 10b69a07..b07e25c9 100644
--- a/Sources/CreateAPI/Generator/Generator+Schemas.swift
+++ b/Sources/CreateAPI/Generator/Generator+Schemas.swift
@@ -173,15 +173,15 @@ extension Generator {
         switch schema.value {
         case .boolean:
             return TypealiasDeclaration(name: name, type: .builtin("Bool"))
-        case .number:
-            return TypealiasDeclaration(name: name, type: .builtin("Double"))
+        case .number(let info, _):
+            return TypealiasDeclaration(name: name, type: numberType(for: info.format))
         case .integer(let info, _):
-            return TypealiasDeclaration(name: name, type: getIntegerType(for: info))
+            return TypealiasDeclaration(name: name, type: integerType(for: info.format))
         case .string(let info, _):
             if isEnum(info) {
                 return try makeStringEnum(name: name, info: info)
             } else {
-                return TypealiasDeclaration(name: name, type: getStringType(for: info))
+                return TypealiasDeclaration(name: name, type: stringType(for: info.format))
             }
         case .object(let info, let details):
             return try makeObject(name: name, info: info, details: details, context: context)
@@ -218,35 +218,6 @@ extension Generator {
         return (decl as? TypealiasDeclaration)?.type
     }
 
-    private func getIntegerType(for info: JSONSchema.CoreContext<JSONTypeFormat.IntegerFormat>) -> TypeIdentifier {
-        guard options.useFixWidthIntegers else {
-            return .builtin("Int")
-        }
-        switch info.format {
-        case .generic, .other: return .builtin("Int")
-        case .int32: return .builtin("Int32")
-        case .int64: return .builtin("Int64")
-        }
-    }
-
-    private func getStringType(for info: JSONSchema.CoreContext<JSONTypeFormat.StringFormat>) -> TypeIdentifier {
-        switch info.format {
-        case .dateTime: return .builtin("Date")
-        case .date: if options.useNaiveDate {
-            setNaiveDateNeeded()
-            return .builtin("NaiveDate")
-        }
-        case .other(let other) where other == "uri":
-            return .builtin("URL")
-        case .other(let other) where other == "uuid":
-            return .builtin("UUID")
-        case .byte:
-          return .builtin("Data")
-        default: break
-        }
-        return .builtin("String")
-    }
-
     private func getReferenceType(_ reference: JSONReference<JSONSchema>, context: Context) throws -> TypeIdentifier {
         switch reference {
         case .internal(let ref):
diff --git a/Sources/CreateOptions/ConfigOptions.swift b/Sources/CreateOptions/ConfigOptions.swift
index 109bd1ef..a9aca934 100644
--- a/Sources/CreateOptions/ConfigOptions.swift
+++ b/Sources/CreateOptions/ConfigOptions.swift
@@ -175,9 +175,6 @@ public struct ConfigOptions: ParsableConfiguration {
     /// Parses dates (e.g. `"2021-09-29"`) using [NaiveDate](https://github.com/CreateAPI/NaiveDate)
     @Option public var useNaiveDate: Bool = true
 
-    /// If enabled, uses `Int64` or `Int32` when specified.
-    @Option public var useFixWidthIntegers: Bool = false
-
     /// Overrides file header comment
     @Option public var fileHeaderComment: String = "// Generated by Create API\n// https://github.com/CreateAPI/CreateAPI"
 
@@ -233,6 +230,35 @@ public struct ConfigOptions: ParsableConfiguration {
         !commentOptions.isEmpty
     }
     
+    public struct DataTypes: Codable {
+        public var string: [String: String]?
+        public var number: [String: String]?
+        public var integer: [String: String]?
+    }
+    
+    /// Change datatype format mapping to Swift types than what CreateAPI provides. Use in combination with [`entity imports`](#entitiesimports), [`paths imports`](#pathsimports), and [`dependencies`](#packagedependencies) for mapping to types that the default library does not provide.
+    ///
+    /// > **Note** `useNaiveDate` takes precedence for overriding the `date` string format. Set `useNaiveDate: false` to properly override this format.
+    ///
+    /// > **Note** Swift types must conform to `Codable`. Incorrect decoding may cause crashes on API calls and incorrect encoding may cause API issues.
+    ///
+    /// <details>
+    /// <summary>Examples</summary>
+    ///
+    /// ```yaml
+    /// datatypes:
+    ///   string:
+    ///     uuid: String
+    ///     phone-number: PhoneNumber # imported type
+    ///   number:
+    ///     float: Double
+    ///   integer:
+    ///     int32: Int
+    ///     int64: Int
+    /// ```
+    /// </details>
+    @Option public var dataTypes = DataTypes()
+    
     @Option public var package: Package
     
     // sourcery: document,
diff --git a/Sources/CreateOptions/Helpers/Issue+Warnings.swift b/Sources/CreateOptions/Helpers/Issue+Warnings.swift
index ecb7ef66..d9c13fdb 100644
--- a/Sources/CreateOptions/Helpers/Issue+Warnings.swift
+++ b/Sources/CreateOptions/Helpers/Issue+Warnings.swift
@@ -34,6 +34,7 @@ extension Issue {
         ["isInliningTypealiases"]: "Replaced by ‘inlineTypealiases‘.",
         ["isPluralizationEnabled"]: "Replaced by ‘pluralizeProperties‘.",
         ["isNaiveDateEnabled"]: "Replaced by ‘useNaiveDate‘.",
+        ["useFixWidthIntegers"]: "Use `dataTypes` instead",
         ["isUsingIntegersWithPredefinedCapacity"]: "Replaced by ‘useFixWidthIntegers‘.",
         ["comments"]: "Replaced by ‘commentOptions‘.",
         ["isSwiftLintDisabled"]: "Add to ‘fileHeaderComment‘ instead.",


Test patch:
diff --git a/Tests/CreateAPITests/GenerateOptionsTests.swift b/Tests/CreateAPITests/GenerateOptionsTests.swift
index 76c1750d..64f297dd 100644
--- a/Tests/CreateAPITests/GenerateOptionsTests.swift
+++ b/Tests/CreateAPITests/GenerateOptionsTests.swift
@@ -370,14 +370,16 @@ final class GenerateOptionsTests: GenerateTestCase {
         )
     }
     
+    // TODO: To be removed/replaced
     func testEdgecasesEnableIntegerCapacity() throws {
         try snapshot(
             spec: .edgecases,
             name: "edgecases-int32-int64",
             configuration: """
-            {
-                "useFixWidthIntegers": true
-            }
+            dataTypes:
+              integer:
+                int32: Int32
+                int64: Int64
             """
         )
     }