66//
77
88import OpenAPIKitCore
9+ import Foundation
910
1011extension OpenAPI {
1112 /// The root of an OpenAPI 3.1 document.
@@ -45,14 +46,17 @@ extension OpenAPI {
4546 ///
4647 /// See the documentation on `DereferencedDocument.resolved()` for more.
4748 ///
48- public struct Document : HasWarnings , CodableVendorExtendable , Sendable {
49+ public struct Document : HasConditionalWarnings , HasWarnings , CodableVendorExtendable , Sendable {
4950 /// OpenAPI Spec "openapi" field.
5051 ///
5152 /// OpenAPIKit only explicitly supports versions that can be found in
5253 /// the `Version` enum. Other versions may or may not be decodable
5354 /// by OpenAPIKit to a certain extent.
5455 public var openAPIVersion : Version
5556
57+ /// OpenAPI Spec "$self" field.
58+ public var selfURI : URL ?
59+
5660 /// Information about the API described by this OpenAPI Document.
5761 ///
5862 /// Licensing, Terms of Service, contact information, API version (the
@@ -142,9 +146,11 @@ extension OpenAPI {
142146 public var vendorExtensions : [ String : AnyCodable ]
143147
144148 public let warnings : [ Warning ]
149+ public let conditionalWarnings : [ ( any Condition , OpenAPI . Warning ) ]
145150
146151 public init (
147152 openAPIVersion: Version = . v3_1_1,
153+ selfURI: URL ? = nil ,
148154 info: Info ,
149155 servers: [ Server ] ,
150156 paths: PathItem . Map ,
@@ -156,6 +162,7 @@ extension OpenAPI {
156162 vendorExtensions: [ String : AnyCodable ] = [ : ]
157163 ) {
158164 self . openAPIVersion = openAPIVersion
165+ self . selfURI = selfURI
159166 self . info = info
160167 self . servers = servers
161168 self . paths = paths
@@ -167,13 +174,28 @@ extension OpenAPI {
167174 self . vendorExtensions = vendorExtensions
168175
169176 self . warnings = [ ]
177+
178+ self . conditionalWarnings = [
179+ // If $self is non-nil, the document must be OAS version 3.2.0 or greater
180+ nonNilVersionWarning ( fieldName: " $self " , value: selfURI, minimumVersion: . v3_2_0) ,
181+ ] . compactMap { $0 }
170182 }
171183 }
172184}
173185
186+ fileprivate func nonNilVersionWarning< Subject> ( fieldName: String , value: Subject ? , minimumVersion: OpenAPI . Document . Version ) -> ( any Condition , OpenAPI . Warning ) ? {
187+ value. map { _ in
188+ OpenAPI . Document. ConditionalWarnings. version (
189+ lessThan: minimumVersion,
190+ doesNotSupport: " The Document \( fieldName) field "
191+ )
192+ }
193+ }
194+
174195extension OpenAPI . Document : Equatable {
175196 public static func == ( lhs: Self , rhs: Self ) -> Bool {
176197 lhs. openAPIVersion == rhs. openAPIVersion
198+ && lhs. selfURI == rhs. selfURI
177199 && lhs. info == rhs. info
178200 && lhs. servers == rhs. servers
179201 && lhs. paths == rhs. paths
@@ -602,6 +624,9 @@ extension OpenAPI.Document: Encodable {
602624 var container = encoder. container ( keyedBy: CodingKeys . self)
603625
604626 try container. encode ( openAPIVersion, forKey: . openAPIVersion)
627+
628+ try container. encodeIfPresent ( selfURI? . absoluteString, forKey: . selfURI)
629+
605630 try container. encode ( info, forKey: . info)
606631
607632 try container. encodeIfPresent ( externalDocs, forKey: . externalDocs)
@@ -661,6 +686,11 @@ extension OpenAPI.Document: Decodable {
661686 )
662687 }
663688
689+ let selfURIString : String ? = try container. decodeIfPresent ( String . self, forKey: . selfURI)
690+ selfURI = try selfURIString. map {
691+ try decodeURIString ( $0, forKey: CodingKeys . selfURI, atPath: decoder. codingPath)
692+ }
693+
664694 info = try container. decode ( OpenAPI . Document. Info. self, forKey: . info)
665695 servers = try container. decodeIfPresent ( [ OpenAPI . Server ] . self, forKey: . servers) ?? [ ]
666696
@@ -681,6 +711,11 @@ extension OpenAPI.Document: Decodable {
681711
682712 self . warnings = warnings
683713
714+ self . conditionalWarnings = [
715+ // If $self is non-nil, the document must be OAS version 3.2.0 or greater
716+ nonNilVersionWarning ( fieldName: " $self " , value: selfURI, minimumVersion: . v3_2_0) ,
717+ ] . compactMap { $0 }
718+
684719 } catch let error as OpenAPI . Error . Decoding . Path {
685720
686721 throw OpenAPI . Error. Decoding. Document ( error)
@@ -697,9 +732,34 @@ extension OpenAPI.Document: Decodable {
697732 }
698733}
699734
735+ fileprivate func decodeURIString( _ str: String , forKey key: CodingKey , atPath path: [ CodingKey ] ) throws -> URL {
736+ let uri : URL ?
737+ #if canImport(FoundationEssentials)
738+ uri = URL ( string: str, encodingInvalidCharacters: false )
739+ #elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
740+ if #available( macOS 14 . 0 , iOS 17 . 0 , watchOS 10 . 0 , tvOS 17 . 0 , * ) {
741+ uri = URL ( string: str, encodingInvalidCharacters: false )
742+ } else {
743+ uri = URL ( string: str)
744+ }
745+ #else
746+ uri = URL ( string: str)
747+ #endif
748+ guard let uri else {
749+ throw GenericError (
750+ subjectName: key. stringValue,
751+ details: " Failed to parse a valid URI from ' \( str) ' " ,
752+ codingPath: path
753+ )
754+ }
755+
756+ return uri
757+ }
758+
700759extension OpenAPI . Document {
701760 internal enum CodingKeys : ExtendableCodingKey {
702761 case openAPIVersion
762+ case selfURI
703763 case info
704764 case jsonSchemaDialect // TODO: implement parsing (https://github.com/mattpolzin/OpenAPIKit/issues/202)
705765 case servers
@@ -714,6 +774,7 @@ extension OpenAPI.Document {
714774 static var allBuiltinKeys : [ CodingKeys ] {
715775 return [
716776 . openAPIVersion,
777+ . selfURI,
717778 . info,
718779 . jsonSchemaDialect,
719780 . servers,
@@ -734,6 +795,8 @@ extension OpenAPI.Document {
734795 switch stringValue {
735796 case " openapi " :
736797 self = . openAPIVersion
798+ case " $self " :
799+ self = . selfURI
737800 case " info " :
738801 self = . info
739802 case " jsonSchemaDialect " :
@@ -761,6 +824,8 @@ extension OpenAPI.Document {
761824 switch self {
762825 case . openAPIVersion:
763826 return " openapi "
827+ case . selfURI:
828+ return " $self "
764829 case . info:
765830 return " info "
766831 case . jsonSchemaDialect:
0 commit comments