Files

190 lines
7.0 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
@propertyWrapper
final class HeapBox<T> {
var wrappedValue: T
init(wrappedValue: T) {
self.wrappedValue = wrappedValue
}
}
/// JSON Schema representation for version draft-07
/// https://json-schema.org/draft-07/draft-handrews-json-schema-01
///
/// NOTE: draft-07 is the latest version of JSON Schema that is supported by
/// most of the tools. We may need to update this schema in the future.
struct JSONSchema: Encodable {
enum CodingKeys: String, CodingKey {
case _schema = "$schema"
case id = "$id"
case comment = "$comment"
case title
case type
case description
case properties
case required
case `enum`
case items
case additionalProperties
case markdownDescription
case markdownEnumDescriptions
case oneOf
case const
}
var _schema: String?
var id: String?
var comment: String?
var title: String?
var type: String?
var description: String?
var properties: [String: JSONSchema]?
var required: [String]?
var `enum`: [String]?
@HeapBox
var items: JSONSchema?
@HeapBox
var additionalProperties: JSONSchema?
/// VSCode extension: Markdown formatted description for rich hover
/// https://github.com/microsoft/vscode-wiki/blob/main/Setting-Descriptions.md
var markdownDescription: String?
/// VSCode extension: Markdown formatted descriptions for rich hover for enum values
/// https://github.com/microsoft/vscode-wiki/blob/main/Setting-Descriptions.md
var markdownEnumDescriptions: [String]?
var oneOf: [JSONSchema]?
var const: String?
func encode(to encoder: any Encoder) throws {
// Manually implement encoding to use `encodeIfPresent` for HeapBox-ed fields
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(_schema, forKey: ._schema)
try container.encodeIfPresent(id, forKey: .id)
try container.encodeIfPresent(comment, forKey: .comment)
try container.encodeIfPresent(title, forKey: .title)
try container.encodeIfPresent(type, forKey: .type)
try container.encodeIfPresent(description, forKey: .description)
if let properties = properties, !properties.isEmpty {
try container.encode(properties, forKey: .properties)
}
if let required = required, !required.isEmpty {
try container.encode(required, forKey: .required)
}
try container.encodeIfPresent(`enum`, forKey: .enum)
try container.encodeIfPresent(items, forKey: .items)
try container.encodeIfPresent(additionalProperties, forKey: .additionalProperties)
try container.encodeIfPresent(markdownDescription, forKey: .markdownDescription)
if let markdownEnumDescriptions {
try container.encode(markdownEnumDescriptions, forKey: .markdownEnumDescriptions)
}
if let oneOf, !oneOf.isEmpty {
try container.encode(oneOf, forKey: .oneOf)
}
try container.encodeIfPresent(const, forKey: .const)
}
}
struct JSONSchemaBuilder {
let context: OptionSchemaContext
func build(from typeSchema: OptionTypeSchama) throws -> JSONSchema {
var schema = try buildJSONSchema(from: typeSchema)
schema._schema = "http://json-schema.org/draft-07/schema#"
return schema
}
private func buildJSONSchema(from typeSchema: OptionTypeSchama) throws -> JSONSchema {
var schema = JSONSchema()
switch typeSchema.kind {
case .boolean: schema.type = "boolean"
case .integer: schema.type = "integer"
case .number: schema.type = "number"
case .string: schema.type = "string"
case .array(let value):
schema.type = "array"
schema.items = try buildJSONSchema(from: value)
case .dictionary(let value):
schema.type = "object"
schema.additionalProperties = try buildJSONSchema(from: value)
case .struct(let structInfo):
schema.type = "object"
var properties: [String: JSONSchema] = [:]
var required: [String] = []
for property in structInfo.properties {
let propertyType = property.type
var propertySchema = try buildJSONSchema(from: propertyType)
propertySchema.description = property.description
// As we usually use Markdown syntax for doc comments, set `markdownDescription`
// too for better rendering in VSCode.
propertySchema.markdownDescription = property.description
properties[property.name] = propertySchema
if !propertyType.isOptional {
required.append(property.name)
}
}
schema.properties = properties
schema.required = required
case .enum(let enumInfo):
let hasAssociatedTypes = enumInfo.cases.contains { !($0.associatedProperties?.isEmpty ?? true) }
if hasAssociatedTypes {
let discriminatorFieldName = enumInfo.discriminatorFieldName ?? "type"
var oneOfSchemas: [JSONSchema] = []
for caseInfo in enumInfo.cases {
var caseSchema = JSONSchema()
caseSchema.type = "object"
caseSchema.description = caseInfo.description
caseSchema.markdownDescription = caseInfo.description
var caseProperties: [String: JSONSchema] = [:]
var caseRequired: [String] = [discriminatorFieldName]
var discriminatorSchema = JSONSchema()
discriminatorSchema.const = caseInfo.name
caseProperties[discriminatorFieldName] = discriminatorSchema
if let associatedProperties = caseInfo.associatedProperties {
for property in associatedProperties {
let propertyType = property.type
var propertySchema = try buildJSONSchema(from: propertyType)
propertySchema.description = property.description
propertySchema.markdownDescription = property.description
caseProperties[property.name] = propertySchema
if !propertyType.isOptional {
caseRequired.append(property.name)
}
}
}
caseSchema.properties = caseProperties
caseSchema.required = caseRequired
oneOfSchemas.append(caseSchema)
}
schema.oneOf = oneOfSchemas
} else {
schema.type = "string"
schema.enum = enumInfo.cases.map(\.name)
// Set `markdownEnumDescriptions` for better rendering in VSCode rich hover
// Unlike `description`, `enumDescriptions` field is not a part of JSON Schema spec,
// so we only set `markdownEnumDescriptions` here.
if enumInfo.cases.contains(where: { $0.description != nil }) {
schema.markdownEnumDescriptions = enumInfo.cases.map { $0.description ?? "" }
}
}
}
return schema
}
}