Skip to content

Commit da5b380

Browse files
authored
Merge pull request #337 from mattpolzin/feature/332/stringformats
String Format Updates
2 parents 4a691fa + ea67126 commit da5b380

15 files changed

+404
-239
lines changed

Sources/OpenAPIKit/Schema Conformances/SwiftPrimitiveTypes+OpenAPI.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,12 @@ extension Int64: OpenAPISchemaType {
7373

7474
extension URL: OpenAPISchemaType {
7575
public static var openAPISchema: JSONSchema {
76-
.string(format: .extended(.uri))
76+
.string(format: .uri)
7777
}
7878
}
7979

8080
extension UUID: OpenAPISchemaType {
8181
public static var openAPISchema: JSONSchema {
82-
.string(format: .extended(.uuid))
82+
.string(format: .uuid)
8383
}
8484
}

Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift

+14-2
Original file line numberDiff line numberDiff line change
@@ -473,15 +473,25 @@ extension JSONSchema.StringContext {
473473
if let conflict = conflicting(pattern, other.pattern) {
474474
throw JSONSchemaResolutionError(.attributeConflict(jsonType: .string, name: "pattern", original: conflict.0, new: conflict.1))
475475
}
476+
if let conflict = conflicting(contentMediaType, other.contentMediaType) {
477+
throw JSONSchemaResolutionError(.attributeConflict(jsonType: .string, name: "contentMediaType", original: conflict.0.rawValue, new: conflict.1.rawValue))
478+
}
479+
if let conflict = conflicting(contentEncoding, other.contentEncoding) {
480+
throw JSONSchemaResolutionError(.attributeConflict(jsonType: .string, name: "contentEncoding", original: conflict.0.rawValue, new: conflict.1.rawValue))
481+
}
476482
// explicitly declaring these constants one at a time
477483
// helps the type checker a lot.
478484
let newMaxLength = maxLength ?? other.maxLength
479485
let newMinLength = Self._minLength(self) ?? Self._minLength(other)
480486
let newPattern = pattern ?? other.pattern
487+
let newContentMediaType = contentMediaType ?? other.contentMediaType
488+
let newContentEncoding = contentEncoding ?? other.contentEncoding
481489
return .init(
482490
maxLength: newMaxLength,
483491
minLength: newMinLength,
484-
pattern: newPattern
492+
pattern: newPattern,
493+
contentMediaType: newContentMediaType,
494+
contentEncoding: newContentEncoding
485495
)
486496
}
487497
}
@@ -660,7 +670,9 @@ extension JSONSchema.StringContext {
660670
return .init(
661671
maxLength: maxLength,
662672
minLength: Self._minLength(self),
663-
pattern: pattern
673+
pattern: pattern,
674+
contentMediaType: contentMediaType,
675+
contentEncoding: contentEncoding
664676
)
665677
}
666678
}

Sources/OpenAPIKit/Schema Object/TypesAndFormats.swift

+102
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,108 @@ public protocol OpenAPIFormat: SwiftTyped, Codable, Equatable, RawRepresentable,
121121
var jsonType: JSONType { get }
122122
}
123123

124+
/// These are just the OpenAPIFormats that are specific to this module; there are shared
125+
/// formats in OpenAPIKitCore/Shared/JSONTypeFormat.swift as well.
126+
extension JSONTypeFormat {
127+
/// The allowed "format" properties for `.string` schemas.
128+
public enum StringFormat: RawRepresentable, Equatable {
129+
case generic
130+
case date
131+
/// A string instance is valid against this attribute if it is a valid
132+
/// date representation as defined by
133+
/// https://tools.ietf.org/html/rfc3339#section-5.6
134+
case dateTime
135+
case duration
136+
case email
137+
case hostname
138+
case idnEmail
139+
case idnHostname
140+
case ipv4
141+
case ipv6
142+
/// International version of .uri
143+
case iri
144+
/// International version of .uriReference
145+
case iriReference
146+
case jsonPointer
147+
case password
148+
case regex
149+
case relativeJsonPointer
150+
case time
151+
/// A string instance is valid against this attribute if it is a valid
152+
/// URI, according to
153+
/// https://tools.ietf.org/html/rfc3986
154+
case uri
155+
/// A string instance is valid against this attribute if it is a valid
156+
/// URI, according to
157+
/// https://tools.ietf.org/html/rfc3986
158+
case uriReference
159+
case uriTemplate
160+
case uuid
161+
case other(String)
162+
163+
public var rawValue: String {
164+
switch self {
165+
case .generic: return ""
166+
case .date: return "date"
167+
case .dateTime: return "date-time"
168+
case .duration: return "duration"
169+
case .email: return "email"
170+
case .hostname: return "hostname"
171+
case .idnEmail: return "idn-email"
172+
case .idnHostname: return "idn-hostname"
173+
case .ipv4: return "ipv4"
174+
case .ipv6: return "ipv6"
175+
case .iri: return "iri"
176+
case .iriReference: return "iri-reference"
177+
case .jsonPointer: return "json-pointer"
178+
case .password: return "password"
179+
case .regex: return "regex"
180+
case .relativeJsonPointer: return "relative-json-pointer"
181+
case .time: return "time"
182+
case .uri: return "uri"
183+
case .uriReference: return "uri-reference"
184+
case .uriTemplate: return "uri-template"
185+
case .uuid: return "uuid"
186+
case .other(let other):
187+
return other
188+
}
189+
}
190+
191+
public init(rawValue: String) {
192+
switch rawValue {
193+
case "": self = .generic
194+
case "date": self = .date
195+
case "date-time": self = .dateTime
196+
case "duration": self = .duration
197+
case "email": self = .email
198+
case "hostname": self = .hostname
199+
case "idn-email": self = .idnEmail
200+
case "idn-hostname": self = .idnHostname
201+
case "ipv4": self = .ipv4
202+
case "ipv6": self = .ipv6
203+
case "iri": self = .iri
204+
case "iri-reference": self = .iriReference
205+
case "json-pointer": self = .jsonPointer
206+
case "password": self = .password
207+
case "regex": self = .regex
208+
case "relative-json-pointer": self = .relativeJsonPointer
209+
case "time": self = .time
210+
case "uri": self = .uri
211+
case "uri-reference": self = .uriReference
212+
case "uri-template": self = .uriTemplate
213+
case "uuid": self = .uuid
214+
default: self = .other(rawValue)
215+
}
216+
}
217+
218+
public typealias SwiftType = String
219+
220+
public static var unspecified: StringFormat {
221+
return .generic
222+
}
223+
}
224+
}
225+
124226
/// A format used when no type is known or any type is allowed.
125227
///
126228
/// There are no built-in formats that do not have an associated

Sources/OpenAPIKit/_CoreReExport.swift

-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ public extension OpenAPI.Response {
4444

4545
public extension JSONSchema {
4646
typealias Permissions = OpenAPIKitCore.Shared.JSONSchemaPermissions
47-
typealias ReferenceContext = OpenAPIKitCore.Shared.ReferenceContext
4847
}
4948

5049
public extension JSONTypeFormat {
@@ -54,5 +53,4 @@ public extension JSONTypeFormat {
5453
typealias ArrayFormat = OpenAPIKitCore.Shared.ArrayFormat
5554
typealias NumberFormat = OpenAPIKitCore.Shared.NumberFormat
5655
typealias IntegerFormat = OpenAPIKitCore.Shared.IntegerFormat
57-
typealias StringFormat = OpenAPIKitCore.Shared.StringFormat
5856
}

Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift

+17
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,23 @@ extension JSONSchema {
601601
return context._minLength
602602
}
603603
}
604+
605+
/// The context that only applies to `.reference` schemas.
606+
public struct ReferenceContext: Equatable {
607+
public let required: Bool
608+
609+
public init(required: Bool = true) {
610+
self.required = required
611+
}
612+
613+
public func requiredContext() -> ReferenceContext {
614+
return .init(required: true)
615+
}
616+
617+
public func optionalContext() -> ReferenceContext {
618+
return .init(required: false)
619+
}
620+
}
604621
}
605622

606623
// MARK: - Codable

Sources/OpenAPIKit30/Schema Object/TypesAndFormats.swift

+75
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,81 @@ public protocol OpenAPIFormat: SwiftTyped, Codable, Equatable, RawRepresentable,
115115
var jsonType: JSONType { get }
116116
}
117117

118+
/// These are just the OpenAPIFormats that are specific to this module; there are shared
119+
/// formats in OpenAPIKitCore/Shared/JSONTypeFormat.swift as well.
120+
extension JSONTypeFormat {
121+
/// The allowed "format" properties for `.string` schemas.
122+
public enum StringFormat: RawRepresentable, Equatable {
123+
case generic
124+
case byte
125+
case binary
126+
case date
127+
/// A string instance is valid against this attribute if it is a valid
128+
/// date representation as defined by
129+
/// https://tools.ietf.org/html/rfc3339#section-5.6
130+
case dateTime
131+
case password
132+
case other(String)
133+
134+
public var rawValue: String {
135+
switch self {
136+
case .generic: return ""
137+
case .byte: return "byte"
138+
case .binary: return "binary"
139+
case .date: return "date"
140+
case .dateTime: return "date-time"
141+
case .password: return "password"
142+
case .other(let other):
143+
return other
144+
}
145+
}
146+
147+
public init(rawValue: String) {
148+
switch rawValue {
149+
case "": self = .generic
150+
case "byte": self = .byte
151+
case "binary": self = .binary
152+
case "date": self = .date
153+
case "date-time": self = .dateTime
154+
case "password": self = .password
155+
default: self = .other(rawValue)
156+
}
157+
}
158+
159+
public typealias SwiftType = String
160+
161+
public static var unspecified: StringFormat {
162+
return .generic
163+
}
164+
}
165+
}
166+
167+
extension JSONTypeFormat.StringFormat {
168+
169+
/// Popular non-standard "format" properties for `.string` schemas.
170+
///
171+
/// Specify with e.g. `.string(format: .extended(.uuid))`
172+
public enum Extended: String, Equatable {
173+
case uuid = "uuid"
174+
case email = "email"
175+
case hostname = "hostname"
176+
case ipv4 = "ipv4"
177+
case ipv6 = "ipv6"
178+
/// A string instance is valid against this attribute if it is a valid
179+
/// URI, according to
180+
/// https://tools.ietf.org/html/rfc3986
181+
case uri = "uri"
182+
/// A string instance is valid against this attribute if it is a valid
183+
/// URI, according to
184+
/// https://tools.ietf.org/html/rfc3986
185+
case uriReference = "uriref"
186+
}
187+
188+
public static func extended(_ format: Extended) -> Self {
189+
return .other(format.rawValue)
190+
}
191+
}
192+
118193
/// A format used when no type is known or any type is allowed.
119194
///
120195
/// There are no built-in formats that do not have an associated

Sources/OpenAPIKit30/_CoreReExport.swift

-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ public extension OpenAPI.Response {
4444

4545
public extension JSONSchema {
4646
typealias Permissions = OpenAPIKitCore.Shared.JSONSchemaPermissions
47-
typealias ReferenceContext = OpenAPIKitCore.Shared.ReferenceContext
4847
}
4948

5049
public extension JSONTypeFormat {
@@ -54,5 +53,4 @@ public extension JSONTypeFormat {
5453
typealias ArrayFormat = OpenAPIKitCore.Shared.ArrayFormat
5554
typealias NumberFormat = OpenAPIKitCore.Shared.NumberFormat
5655
typealias IntegerFormat = OpenAPIKitCore.Shared.IntegerFormat
57-
typealias StringFormat = OpenAPIKitCore.Shared.StringFormat
5856
}

Sources/OpenAPIKitCompat/Compat30To31.swift

+39-6
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,12 @@ extension OpenAPIKit30.OpenAPI.SecurityScheme: To31 {
471471
}
472472
}
473473

474+
extension OpenAPIKit30.JSONTypeFormat.StringFormat: To31 {
475+
fileprivate func to31() -> OpenAPIKit.JSONTypeFormat.StringFormat {
476+
.init(rawValue: rawValue)
477+
}
478+
}
479+
474480
extension OpenAPIKit30.JSONTypeFormat: To31 {
475481
fileprivate func to31() -> OpenAPIKit.JSONTypeFormat {
476482
switch self {
@@ -485,7 +491,7 @@ extension OpenAPIKit30.JSONTypeFormat: To31 {
485491
case .integer(let f):
486492
return .integer(f)
487493
case .string(let f):
488-
return .string(f)
494+
return .string(f.to31())
489495
}
490496
}
491497
}
@@ -509,6 +515,25 @@ extension OpenAPIKit30.JSONSchema.CoreContext: To31 where Format: OpenAPIKit.Ope
509515
}
510516
}
511517

518+
extension OpenAPIKit30.JSONSchema.CoreContext where Format == OpenAPIKit30.JSONTypeFormat.StringFormat {
519+
fileprivate func to31() -> OpenAPIKit.JSONSchema.CoreContext<OpenAPIKit.JSONTypeFormat.StringFormat> {
520+
OpenAPIKit.JSONSchema.CoreContext<OpenAPIKit.JSONTypeFormat.StringFormat>(
521+
format: format.to31(),
522+
required: `required`,
523+
nullable: nullable,
524+
permissions: permissions,
525+
deprecated: deprecated,
526+
title: title,
527+
description: description,
528+
discriminator: discriminator,
529+
externalDocs: externalDocs?.to31(),
530+
allowedValues: allowedValues,
531+
defaultValue: defaultValue,
532+
examples: [example].compactMap { $0 }
533+
)
534+
}
535+
}
536+
512537
extension OpenAPIKit30.JSONSchema.NumericContext: To31 {
513538
fileprivate func to31() -> OpenAPIKit.JSONSchema.NumericContext {
514539
OpenAPIKit.JSONSchema.NumericContext(
@@ -551,12 +576,20 @@ extension OpenAPIKit30.JSONSchema.ObjectContext: To31 {
551576
}
552577
}
553578

554-
extension OpenAPIKit30.JSONSchema.StringContext: To31 {
555-
fileprivate func to31() -> OpenAPIKit.JSONSchema.StringContext {
556-
OpenAPIKit.JSONSchema.StringContext(
579+
extension OpenAPIKit30.JSONSchema.StringContext {
580+
fileprivate func to31(format: OpenAPIKit30.JSONTypeFormat.StringFormat) -> OpenAPIKit.JSONSchema.StringContext {
581+
let contentEncoding: OpenAPIKit.OpenAPI.ContentEncoding?
582+
switch format {
583+
case .byte: contentEncoding = .base64
584+
case .binary: contentEncoding = .binary
585+
default: contentEncoding = nil
586+
}
587+
588+
return OpenAPIKit.JSONSchema.StringContext(
557589
maxLength: maxLength,
558590
minLength: OpenAPIKit30.JSONSchema.StringContext._minLength(self),
559-
pattern: pattern
591+
pattern: pattern,
592+
contentEncoding: contentEncoding
560593
)
561594
}
562595
}
@@ -573,7 +606,7 @@ extension OpenAPIKit30.JSONSchema: To31 {
573606
case .integer(let core, let integral):
574607
schema = .integer(core.to31(), integral.to31())
575608
case .string(let core, let stringy):
576-
schema = .string(core.to31(), stringy.to31())
609+
schema = .string(core.to31(), stringy.to31(format: core.format))
577610
case .object(let core, let objective):
578611
schema = .object(core.to31(), objective.to31())
579612
case .array(let core, let listy):

0 commit comments

Comments
 (0)