Schema.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. // Copyright 2024 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import Foundation
  15. /// A `Schema` object allows the definition of input and output data types.
  16. ///
  17. /// These types can be objects, but also primitives and arrays. Represents a select subset of an
  18. /// [OpenAPI 3.0 schema object](https://spec.openapis.org/oas/v3.0.3#schema).
  19. public class Schema {
  20. /// Modifiers describing the expected format of a string `Schema`.
  21. public enum StringFormat {
  22. /// A custom string format.
  23. case custom(String)
  24. }
  25. /// Modifiers describing the expected format of an integer `Schema`.
  26. public enum IntegerFormat {
  27. /// A 32-bit signed integer.
  28. case int32
  29. /// A 64-bit signed integer.
  30. case int64
  31. /// A custom integer format.
  32. case custom(String)
  33. }
  34. /// The data type.
  35. public let type: DataType
  36. /// The format of the data.
  37. public let format: String?
  38. /// A brief description of the parameter.
  39. public let description: String?
  40. /// Indicates if the value may be null.
  41. public let nullable: Bool?
  42. /// Possible values of the element of type ``DataType/string`` with "enum" format.
  43. public let enumValues: [String]?
  44. /// Schema of the elements of type ``DataType/array``.
  45. public let items: Schema?
  46. /// Properties of type ``DataType/object``.
  47. public let properties: [String: Schema]?
  48. /// Required properties of type ``DataType/object``.
  49. public let requiredProperties: [String]?
  50. /// Constructs a new `Schema`.
  51. ///
  52. /// - Parameters:
  53. /// - type: The data type.
  54. /// - format: The format of the data; used only for primitive datatypes.
  55. /// Supported formats:
  56. /// - ``DataType/integer``: int32, int64
  57. /// - ``DataType/number``: float, double
  58. /// - ``DataType/string``: enum
  59. /// - description: A brief description of the parameter; may be formatted as Markdown.
  60. /// - nullable: Indicates if the value may be null.
  61. /// - enumValues: Possible values of the element of type ``DataType/string`` with "enum" format.
  62. /// For example, an enum `Direction` may be defined as `["EAST", NORTH", "SOUTH", "WEST"]`.
  63. /// - items: Schema of the elements of type ``DataType/array``.
  64. /// - properties: Properties of type ``DataType/object``.
  65. /// - requiredProperties: Required properties of type ``DataType/object``.
  66. @available(*, deprecated, message: """
  67. Use static methods `string(description:format:nullable:)`, `number(description:format:nullable:)`,
  68. etc., instead.
  69. """)
  70. public convenience init(type: DataType, format: String? = nil, description: String? = nil,
  71. nullable: Bool? = nil, enumValues: [String]? = nil, items: Schema? = nil,
  72. properties: [String: Schema]? = nil,
  73. requiredProperties: [String]? = nil) {
  74. self.init(
  75. type: type,
  76. format: format,
  77. description: description,
  78. nullable: nullable ?? false,
  79. enumValues: enumValues,
  80. items: items,
  81. properties: properties,
  82. requiredProperties: requiredProperties
  83. )
  84. }
  85. required init(type: DataType, format: String? = nil, description: String? = nil,
  86. nullable: Bool = false, enumValues: [String]? = nil, items: Schema? = nil,
  87. properties: [String: Schema]? = nil, requiredProperties: [String]? = nil) {
  88. self.type = type
  89. self.format = format
  90. self.description = description
  91. self.nullable = nullable
  92. self.enumValues = enumValues
  93. self.items = items
  94. self.properties = properties
  95. self.requiredProperties = requiredProperties
  96. }
  97. /// Returns a `Schema` representing a string value.
  98. ///
  99. /// This schema instructs the model to produce data of type ``DataType/string``, which is suitable
  100. /// for decoding into a Swift `String` (or `String?`, if `nullable` is set to `true`).
  101. ///
  102. /// > Tip: If a specific set of string values should be generated by the model (for example,
  103. /// > "north", "south", "east", or "west"), use ``enumeration(values:description:nullable:)``
  104. /// > instead to constrain the generated values.
  105. ///
  106. /// - Parameters:
  107. /// - description: An optional description of what the string should contain or represent; may
  108. /// use Markdown format.
  109. /// - nullable: If `true`, instructs the model that it *may* generate `null` instead of a
  110. /// string; defaults to `false`, enforcing that a string value is generated.
  111. /// - format: An optional modifier describing the expected format of the string. Currently no
  112. /// formats are officially supported for strings but custom values may be specified using
  113. /// ``StringFormat/custom(_:)``, for example `.custom("email")` or `.custom("byte")`; these
  114. /// provide additional hints for how the model should respond but are not guaranteed to be
  115. /// adhered to.
  116. public static func string(description: String? = nil, nullable: Bool = false,
  117. format: StringFormat? = nil) -> Schema {
  118. return self.init(
  119. type: .string,
  120. format: format?.rawValue,
  121. description: description,
  122. nullable: nullable
  123. )
  124. }
  125. /// Returns a `Schema` representing an enumeration of string values.
  126. ///
  127. /// This schema instructs the model to produce data of type ``DataType/string`` with the
  128. /// `format` `"enum"`. This data is suitable for decoding into a Swift `String` (or `String?`,
  129. /// if `nullable` is set to `true`), or an `enum` with strings as raw values.
  130. ///
  131. /// **Example:**
  132. /// The values `["north", "south", "east", "west"]` for an enumation of directions.
  133. /// ```
  134. /// enum Direction: String, Decodable {
  135. /// case north, south, east, west
  136. /// }
  137. /// ```
  138. ///
  139. /// - Parameters:
  140. /// - values: The list of string values that may be generated by the model.
  141. /// - description: An optional description of what the `values` contain or represent; may use
  142. /// Markdown format.
  143. /// - nullable: If `true`, instructs the model that it *may* generate `null` instead of one of
  144. /// the strings specified in `values`; defaults to `false`, enforcing that one of the string
  145. /// values is generated.
  146. public static func enumeration(values: [String], description: String? = nil,
  147. nullable: Bool = false) -> Schema {
  148. return self.init(
  149. type: .string,
  150. format: "enum",
  151. description: description,
  152. nullable: nullable,
  153. enumValues: values
  154. )
  155. }
  156. /// Returns a `Schema` representing a single-precision floating-point number.
  157. ///
  158. /// This schema instructs the model to produce data of type ``DataType/number`` with the
  159. /// `format` `"float"`, which is suitable for decoding into a Swift `Float` (or `Float?`, if
  160. /// `nullable` is set to `true`).
  161. ///
  162. /// > Important: This `Schema` provides a hint to the model that it should generate a
  163. /// > single-precision floating-point number, a `float`, but only guarantees that the value will
  164. /// > be a number.
  165. ///
  166. /// - Parameters:
  167. /// - description: An optional description of what the number should contain or represent; may
  168. /// use Markdown format.
  169. /// - nullable: If `true`, instructs the model that it may generate `null` instead of a number;
  170. /// defaults to `false`, enforcing that a number is generated.
  171. public static func float(description: String? = nil, nullable: Bool = false) -> Schema {
  172. return self.init(
  173. type: .number,
  174. format: "float",
  175. description: description,
  176. nullable: nullable
  177. )
  178. }
  179. /// Returns a `Schema` representing a double-precision floating-point number.
  180. ///
  181. /// This schema instructs the model to produce data of type ``DataType/number`` with the
  182. /// `format` `"double"`, which is suitable for decoding into a Swift `Double` (or `Double?`, if
  183. /// `nullable` is set to `true`).
  184. ///
  185. /// > Important: This `Schema` provides a hint to the model that it should generate a
  186. /// > double-precision floating-point number, a `double`, but only guarantees that the value will
  187. /// > be a number.
  188. ///
  189. /// - Parameters:
  190. /// - description: An optional description of what the number should contain or represent; may
  191. /// use Markdown format.
  192. /// - nullable: If `true`, instructs the model that it may return `null` instead of a number;
  193. /// defaults to `false`, enforcing that a number is returned.
  194. public static func double(description: String? = nil, nullable: Bool = false) -> Schema {
  195. return self.init(
  196. type: .number,
  197. format: "double",
  198. description: description,
  199. nullable: nullable
  200. )
  201. }
  202. /// Returns a `Schema` representing an integer value.
  203. ///
  204. /// This schema instructs the model to produce data of type ``DataType/integer``, which is
  205. /// suitable for decoding into a Swift `Int` (or `Int?`, if `nullable` is set to `true`) or other
  206. /// integer types (such as `Int32`) based on the expected size of values being generated.
  207. ///
  208. /// > Important: If a `format` of ``IntegerFormat/int32`` or ``IntegerFormat/int64`` is
  209. /// > specified, this provides a hint to the model that it should generate 32-bit or 64-bit
  210. /// > integers but this `Schema` only guarantees that the value will be an integer. Therefore, it
  211. /// > is *possible* that decoding into an `Int32` could overflow even if a `format` of
  212. /// > ``IntegerFormat/int32`` is specified.
  213. ///
  214. /// - Parameters:
  215. /// - description: An optional description of what the integer should contain or represent; may
  216. /// use Markdown format.
  217. /// - nullable: If `true`, instructs the model that it may return `null` instead of an integer;
  218. /// defaults to `false`, enforcing that an integer is returned.
  219. /// - format: An optional modifier describing the expected format of the integer. Currently the
  220. /// formats ``IntegerFormat/int32`` and ``IntegerFormat/int64`` are supported; custom values
  221. /// may be specified using ``IntegerFormat/custom(_:)`` but may be ignored by the model.
  222. public static func integer(description: String? = nil, nullable: Bool = false,
  223. format: IntegerFormat? = nil) -> Schema {
  224. return self.init(
  225. type: .integer,
  226. format: format?.rawValue,
  227. description: description,
  228. nullable: nullable
  229. )
  230. }
  231. /// Returns a `Schema` representing a boolean value.
  232. ///
  233. /// This schema instructs the model to produce data of type ``DataType/boolean``, which is
  234. /// suitable for decoding into a Swift `Bool` (or `Bool?`, if `nullable` is set to `true`).
  235. ///
  236. /// - Parameters:
  237. /// - description: An optional description of what the boolean should contain or represent; may
  238. /// use Markdown format.
  239. /// - nullable: If `true`, instructs the model that it may return `null` instead of a boolean;
  240. /// defaults to `false`, enforcing that a boolean is returned.
  241. public static func boolean(description: String? = nil, nullable: Bool = false) -> Schema {
  242. return self.init(type: .boolean, description: description, nullable: nullable)
  243. }
  244. /// Returns a `Schema` representing an array.
  245. ///
  246. /// This schema instructs the model to produce data of type ``DataType/array``, which has elements
  247. /// of any other ``DataType`` (including nested ``DataType/array``s). This data is suitable for
  248. /// decoding into many Swift collection types, including `Array`, holding elements of types
  249. /// suitable for decoding from the respective `items` type.
  250. ///
  251. /// - Parameters:
  252. /// - items: The `Schema` of the elements that the array will hold.
  253. /// - description: An optional description of what the array should contain or represent; may
  254. /// use Markdown format.
  255. /// - nullable: If `true`, instructs the model that it may return `null` instead of an array;
  256. /// defaults to `false`, enforcing that an array is returned.
  257. public static func array(items: Schema, description: String? = nil,
  258. nullable: Bool = false) -> Schema {
  259. return self.init(type: .array, description: description, nullable: nullable, items: items)
  260. }
  261. /// Returns a `Schema` representing an object.
  262. ///
  263. /// This schema instructs the model to produce data of type ``DataType/object``, which has keys
  264. /// of type ``DataType/string`` and values of any other ``DataType`` (including nested
  265. /// ``DataType/object``s). This data is suitable for decoding into Swift keyed collection types,
  266. /// including `Dictionary`, or other custom `struct` or `class` types.
  267. ///
  268. /// **Example:** A `City` could be represented with the following object `Schema`.
  269. /// ```
  270. /// Schema.object(properties: [
  271. /// "name" : .string(),
  272. /// "population": .integer()
  273. /// ])
  274. /// ```
  275. /// The generated data could be decoded into a Swift native type:
  276. /// ```
  277. /// struct City: Decodable {
  278. /// let name: String
  279. /// let population: Int
  280. /// }
  281. /// ```
  282. ///
  283. /// - Parameters:
  284. /// - properties: A dictionary containing the object's property names as keys and their
  285. /// respective `Schema`s as values.
  286. /// - optionalProperties: A list of property names that may be be omitted in objects generated
  287. /// by the model; these names must correspond to the keys provided in the `properties`
  288. /// dictionary and may be an empty list.
  289. /// - description: An optional description of what the object should contain or represent; may
  290. /// use Markdown format.
  291. /// - nullable: If `true`, instructs the model that it may return `null` instead of an object;
  292. /// defaults to `false`, enforcing that an object is returned.
  293. public static func object(properties: [String: Schema], optionalProperties: [String] = [],
  294. description: String? = nil, nullable: Bool = false) -> Schema {
  295. var requiredProperties = Set(properties.keys)
  296. for optionalProperty in optionalProperties {
  297. guard properties.keys.contains(optionalProperty) else {
  298. fatalError("Optional property \"\(optionalProperty)\" not defined in object properties.")
  299. }
  300. requiredProperties.remove(optionalProperty)
  301. }
  302. return self.init(
  303. type: .object,
  304. description: description,
  305. nullable: nullable,
  306. properties: properties,
  307. requiredProperties: requiredProperties.sorted()
  308. )
  309. }
  310. }
  311. /// A data type.
  312. ///
  313. /// Contains the set of OpenAPI [data types](https://spec.openapis.org/oas/v3.0.3#data-types).
  314. public enum DataType: String {
  315. /// A `String` type.
  316. case string = "STRING"
  317. /// A floating-point number type.
  318. case number = "NUMBER"
  319. /// An integer type.
  320. case integer = "INTEGER"
  321. /// A boolean type.
  322. case boolean = "BOOLEAN"
  323. /// An array type.
  324. case array = "ARRAY"
  325. /// An object type.
  326. case object = "OBJECT"
  327. }
  328. // MARK: - Codable Conformance
  329. extension Schema: Encodable {
  330. enum CodingKeys: String, CodingKey {
  331. case type
  332. case format
  333. case description
  334. case nullable
  335. case enumValues = "enum"
  336. case items
  337. case properties
  338. case requiredProperties = "required"
  339. }
  340. }
  341. extension DataType: Encodable {}
  342. // MARK: - RawRepresentable Conformance
  343. extension Schema.IntegerFormat: RawRepresentable {
  344. public init?(rawValue: String) {
  345. switch rawValue {
  346. case "int32":
  347. self = .int32
  348. case "int64":
  349. self = .int64
  350. default:
  351. self = .custom(rawValue)
  352. }
  353. }
  354. public var rawValue: String {
  355. switch self {
  356. case .int32:
  357. return "int32"
  358. case .int64:
  359. return "int64"
  360. case let .custom(format):
  361. return format
  362. }
  363. }
  364. }
  365. extension Schema.StringFormat: RawRepresentable {
  366. public init?(rawValue: String) {
  367. switch rawValue {
  368. default:
  369. self = .custom(rawValue)
  370. }
  371. }
  372. public var rawValue: String {
  373. switch self {
  374. case let .custom(format):
  375. return format
  376. }
  377. }
  378. }