diff --git a/README.md b/README.md index 93937098..97c88160 100644 --- a/README.md +++ b/README.md @@ -62,16 +62,6 @@ let token = try jwt.sign(using: p8) // request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") ``` -## Development - -Download OpenAPISpecification from https://developer.apple.com/documentation/appstoreconnectapi. then, - -```sh -git clone https://github.com/swiftty/AppStoreConnectKit.git -cd AppStoreConnectKit/tools - swift run appstoreconnectgen --open-api-path $path_to_open_api.json --output ../Sources/AppStoreConnectKit/autogenerated/ -``` - ## License AppStoreConnectKit is available under the MIT license, and uses source code from open source projects. See the [LICENSE](https://github.com/swiftty/AppStoreConnectKit/blob/main/LICENSE) file for more info. diff --git a/tools/.gitignore b/tools/.gitignore deleted file mode 100644 index bb460e7b..00000000 --- a/tools/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -.DS_Store -/.build -/Packages -/*.xcodeproj -xcuserdata/ -DerivedData/ -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/tools/.swiftlint.yml b/tools/.swiftlint.yml deleted file mode 100644 index 0b16e9dc..00000000 --- a/tools/.swiftlint.yml +++ /dev/null @@ -1,4 +0,0 @@ -included: - - Package.swift - - Sources - - Tests diff --git a/tools/Package.resolved b/tools/Package.resolved deleted file mode 100644 index a644d5f1..00000000 --- a/tools/Package.resolved +++ /dev/null @@ -1,16 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "swift-argument-parser", - "repositoryURL": "https://github.com/apple/swift-argument-parser.git", - "state": { - "branch": null, - "revision": "8f4d2753f0e4778c76d5f05ad16c74f707390531", - "version": "1.2.3" - } - } - ] - }, - "version": 1 -} diff --git a/tools/Package.swift b/tools/Package.swift deleted file mode 100644 index 0c41499c..00000000 --- a/tools/Package.swift +++ /dev/null @@ -1,42 +0,0 @@ -// swift-tools-version:5.5 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "tools", - platforms: [.macOS(.v11)], - products: [ - .executable( - name: "appstoreconnectgen", - targets: ["appstoreconnectgen"]) - ], - dependencies: [ - .package( - url: "https://github.com/apple/swift-argument-parser.git", - from: "1.4.0") - ], - targets: [ - .executableTarget( - name: "appstoreconnectgen", - dependencies: [ - "AppStoreConnectGenKit", - "AppStoreConnectGenForSwift", - .product(name: "ArgumentParser", package: "swift-argument-parser") - ]), - - .target( - name: "AppStoreConnectGenForSwift", - dependencies: [ - "AppStoreConnectGenKit" - ]), - - .testTarget( - name: "AppStoreConnectGenForSwiftTests", - dependencies: [ - "AppStoreConnectGenForSwift" - ]), - - .target(name: "AppStoreConnectGenKit") - ] -) diff --git a/tools/README.md b/tools/README.md deleted file mode 100644 index 68f1032b..00000000 --- a/tools/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# tools - -A description of this package. diff --git a/tools/Sources/AppStoreConnectGenForSwift/Decl.swift b/tools/Sources/AppStoreConnectGenForSwift/Decl.swift deleted file mode 100644 index caaa7798..00000000 --- a/tools/Sources/AppStoreConnectGenForSwift/Decl.swift +++ /dev/null @@ -1,432 +0,0 @@ -import Foundation - -protocol Decl { - func render(context: inout RenderContext, in namespaces: [String]) -> String -} - -enum AccessLevel { - case `public`, `private`, `internal` -} - -struct TypealiasDecl: Decl { - var access: AccessLevel - var name: String - var value: String - var generics: String? - var whereClause: String? - - func render(context: inout RenderContext, in namespaces: [String]) -> String { - func renderGenerics() -> String { - guard let generics = generics else { return "" } - return "<\(generics)>" - - } - func renderWhereClause() -> String { - guard let whereClause = whereClause else { return "" } - return " where \(whereClause)" - } - return "\(access) typealias \(name)\(renderGenerics()) = \(value)\(renderWhereClause())" - } -} - -struct AnnotationDecl: Decl { - static func deprecated() -> Self { - Self.init(body: "@available(*, deprecated)") - } - - var body: String - - func render(context: inout RenderContext, in namespaces: [String]) -> String { - body - } -} - -struct SubscriptDecl: Decl { - var access: AccessLevel - var generics: String? - var arguments: [ArgumentDecl] - var returnType: String? - var getter: String - var setter: String? - - func render(context: inout RenderContext, in namespaces: [String]) -> String { - func renderGenerics() -> String { - guard let generics = generics else { return "" } - return "<\(generics)>" - - } - func renderArguments() -> String { - guard !arguments.isEmpty else { return "" } - return arguments.map { $0.render(context: &context, in: namespaces) }.joined(separator: ", ") - } - func renderReturnType() -> String { - guard let returnType = returnType else { return "" } - return " -> \(returnType)" - } - func renderSetter() -> String { - guard let setter = setter else { return "" } - return "set { \(setter) }" - } - - let body: String - if let setter = setter { - body = """ - get { \(getter) } - set { \(setter) } - """ - } else { - body = """ - \(getter) - """ - } - return """ - \(access) subscript \(renderGenerics())(\(renderArguments()))\(renderReturnType()) { - \(body.indent(to: 4)) - } - """ - } -} - -struct MemberDecl: Decl { - enum Modifier { - case `static` - } - enum Keyword { - case `let`, `var` - } - enum Value { - case assignment(String) - case computed(String) - } - var annotations: [AnnotationDecl] = [] - var access: AccessLevel - var modifier: Modifier? - var keyword: Keyword - var name: String - var type: String - var value: Value? - var doc: String? - - func render(context: inout RenderContext, in namespaces: [String]) -> String { - func renderDoc() -> String { - guard let doc = doc else { return "" } - return "/// " + doc - .components(separatedBy: "\n") - .joined(separator: "\n/// ") - } - func renderAnnotations() -> String { - guard !annotations.isEmpty else { return "" } - return "\n" + annotations.map { $0.render(context: &context, in: namespaces) }.joined(separator: "\n") - } - func renderModifier() -> String { - guard let modifier = modifier else { return "" } - return "\(modifier) " - } - func renderValue() -> String { - switch value { - case .assignment(let value): - return " = \(value)" - case .computed(let body): - return """ - { - \(body.indent(to: 4)) - } - """ - case nil: - return "" - } - } - return """ - \(renderDoc())\(renderAnnotations()) - \(access) \(renderModifier())\(keyword) \(name): \(type)\(renderValue()) - """.trimmingCharacters(in: .whitespacesAndNewlines) - } -} - -struct ArgumentDecl: Decl { - var name: String - var alt: String? - var type: String - var initial: String? - - func render(context: inout RenderContext, in namespaces: [String]) -> String { - func renderAlt() -> String { - guard let alt = alt else { return "" } - return "\(alt) " - } - func renderValue() -> String { - guard let initial = initial else { return "" } - return " = \(initial)" - } - - if name.isEmpty { - return "\(type)\(renderValue())" - } - return """ - \(renderAlt())\(name): \(type)\(renderValue()) - """ - } -} - -struct InitializerDecl: Decl { - typealias ParameterModifier = FunctionDecl.ParameterModifier - var access: AccessLevel - var arguments: [ArgumentDecl] - var modifiers: [ParameterModifier]? - var body: String - - func render(context: inout RenderContext, in namespaces: [String]) -> String { - func renderArguments() -> String { - guard !arguments.isEmpty else { return "" } - if arguments.count > 1 { - return "\n" - + arguments - .map { $0.render(context: &context, in: namespaces) } - .joined(separator: ",\n") - .indent(to: 4) - + "\n" - } else { - return arguments.map { $0.render(context: &context, in: namespaces) }.joined(separator: ", ") - } - } - func renderModifiers() -> String { - guard let modifiers = modifiers, !modifiers.isEmpty else { return "" } - return modifiers.map { "\($0)" }.joined(separator: " ") + " " - } - return """ - \(access) init(\(renderArguments())) \(renderModifiers()){ - \(body.indent(to: 4)) - } - """ - } -} - -struct FunctionDecl: Decl { - enum DeclModifier { - case `static`, `class` - } - enum ParameterModifier { - case `throws` - } - var access: AccessLevel - var declModifier: DeclModifier? - var name: String - var arguments: [ArgumentDecl] - var parameterModifiers: [ParameterModifier]? - var returnType: String? - var body: String - var doc: String? - - func render(context: inout RenderContext, in namespaces: [String]) -> String { - func renderDoc() -> String { - guard let doc = doc else { return "" } - return "/// \(doc.components(separatedBy: "\n").joined(separator: "\n/// "))" - } - func renderDeclModifier() -> String { - guard let declModifier = declModifier else { return "" } - return " \(declModifier)" - } - func renderArguments() -> String { - guard !arguments.isEmpty else { return "" } - return arguments.map { $0.render(context: &context, in: namespaces) }.joined(separator: ", ") - } - func componentsForModifiers() -> [String] { - guard let modifiers = parameterModifiers, !modifiers.isEmpty else { return [] } - return modifiers.map { "\($0)" } - } - func componentsForReturnType() -> [String] { - guard let returnType = returnType else { return [] } - return ["->", returnType] - } - func renderModifiersAndReturnType() -> String { - let comps = componentsForModifiers() + componentsForReturnType() - guard !comps.isEmpty else { return "" } - return comps.joined(separator: " ") + " " - } - return """ - \(renderDoc()) - \(access)\(renderDeclModifier()) func \(name)(\(renderArguments())) \(renderModifiersAndReturnType()){ - \(body.indent(to: 4)) - } - """.trimmingCharacters(in: .whitespacesAndNewlines) - } -} - -struct StructDecl: Decl { - var annotations: [AnnotationDecl] = [] - var access: AccessLevel - var name: String - var inheritances: [String] = [] - var typealiases: [TypealiasDecl] = [] - var subscripts: [SubscriptDecl] = [] - var members: [MemberDecl] - - var initializers: [InitializerDecl] - var functions: [FunctionDecl] - - var nested: [Decl] = [] - var extensions: [Decl] = [] - - func render(context: inout RenderContext, in namespaces: [String]) -> String { - let namespaces = namespaces + [name] - context.insert(extensions: extensions, with: namespaces) - - func render(_ arr: [Decl], separator: String = "\n\n") -> String { - let content = arr - .map { $0.render(context: &context, in: namespaces) } - .joined(separator: separator) - return content + (arr.isEmpty ? "" : "\n\n") - } - var body: String = "" - body += render(typealiases, separator: "\n") - body += render(subscripts) - body += render(members) - body += render(initializers) - body += render(functions) - body += render(nested) - body = body.trimmingCharacters(in: .whitespacesAndNewlines) - - return """ - \(annotations.map { - $0.render(context: &context, in: namespaces) - }.joined(separator: "\n")) - \(access) struct \(name)\ - \(inheritances.isEmpty ? "" : ": " + inheritances.joined(separator: ", ")) { - \(body.indent(to: 4)) - } - """.trimmingCharacters(in: .whitespacesAndNewlines) - } -} - -struct CaseDecl: Decl { - enum Value { - case int(Int) - case string(String) - case arguments([ArgumentDecl]) - } - var name: String - var value: Value? - - func render(context: inout RenderContext, in namespaces: [String]) -> String { - func renderValue() -> String { - switch value { - case .int(let value): - return " = \(value)" - case .string(let value): - return #" = "\#(value)""# - case .arguments(let args): - return """ - (\(args.map { $0.render(context: &context, in: namespaces) }.joined(separator: ", "))) - """ - case nil: - return "" - } - } - return """ - case \(name)\(renderValue()) - """ - } -} - -struct EnumDecl: Decl { - var annotations: [AnnotationDecl] = [] - var access: AccessLevel - var name: String - var inheritances: [String] = [] - var typealiases: [TypealiasDecl] = [] - var cases: [CaseDecl] - - var initializers: [InitializerDecl] = [] - var members: [MemberDecl] = [] - var functions: [FunctionDecl] = [] - - var nested: [Decl] = [] - var extensions: [Decl] = [] - - func render(context: inout RenderContext, in namespaces: [String]) -> String { - let namespaces = namespaces + [name] - context.insert(extensions: extensions, with: namespaces) - - let interface = """ - \(access) enum \(name)\ - \(inheritances.isEmpty ? "" : ": " + inheritances.joined(separator: ", ")) - """ - - func render(_ arr: [Decl], separator: String = "\n\n") -> String { - let content = arr - .map { $0.render(context: &context, in: namespaces) } - .joined(separator: separator) - return content + (arr.isEmpty ? "" : "\n\n") - } - var body: String = "" - body += render(typealiases, separator: "\n") - body += render(cases, separator: "\n") - body += render(members) - body += render(initializers) - body += render(functions) - body += render(nested) - body = body.trimmingCharacters(in: .whitespacesAndNewlines) - - return """ - \(annotations.map { - $0.render(context: &context, in: namespaces) - }.joined(separator: "\n")) - \(interface) \(body.isEmpty ? "{}" : """ - { - \(body.indent(to: 4)) - } - """) - """.trimmingCharacters(in: .whitespacesAndNewlines) - } -} - -struct ExtensionDecl: Decl { - var name: String - var inheritances: [String] = [] - var body: [Decl] - - func render(context: inout RenderContext, in namespaces: [String]) -> String { - return """ - extension \(namespaces.isEmpty ? "" : namespaces.joined(separator: ".") + ".")\(name)\ - \(inheritances.isEmpty ? "" : ": " + inheritances.joined(separator: ", ")) { - \(body - .map { $0.render(context: &context, in: name.components(separatedBy: ".")) } - .joined(separator: "\n\n") - .indent(to: 4) - .trimmingCharacters(in: .whitespacesAndNewlines)) - } - """ - } -} - -struct RenderContext { - var extensions: [(`extension`: Decl, namespaces: [String])] = [] - - mutating func insert(extensions: [Decl], with namespaces: [String]) { - extensions.forEach { - self.extensions.append(($0, namespaces)) - } - } -} - -struct SourceFile { - let decl: Decl - - func render() -> String { - var context = RenderContext() - var result = decl.render(context: &context, in: []) + "\n\n" - - func renderExtension(context: RenderContext) { - for (decl, namespaces) in context.extensions where !namespaces.isEmpty { - let ext = ExtensionDecl(name: namespaces.joined(separator: "."), body: [decl]) - var childContext = RenderContext() - result += ext.render(context: &childContext, in: []) + "\n\n" - renderExtension(context: childContext) - } - } - - renderExtension(context: context) - - return result.trimmingCharacters(in: .whitespacesAndNewlines) - } -} diff --git a/tools/Sources/AppStoreConnectGenForSwift/EndpointInterfaceRenderer.swift b/tools/Sources/AppStoreConnectGenForSwift/EndpointInterfaceRenderer.swift deleted file mode 100644 index fecc4ab4..00000000 --- a/tools/Sources/AppStoreConnectGenForSwift/EndpointInterfaceRenderer.swift +++ /dev/null @@ -1 +0,0 @@ -import Foundation diff --git a/tools/Sources/AppStoreConnectGenForSwift/EndpointNamespaceRenderer.swift b/tools/Sources/AppStoreConnectGenForSwift/EndpointNamespaceRenderer.swift deleted file mode 100644 index c830e264..00000000 --- a/tools/Sources/AppStoreConnectGenForSwift/EndpointNamespaceRenderer.swift +++ /dev/null @@ -1,82 +0,0 @@ -import Foundation -import AppStoreConnectGenKit - -struct EndpointNamespaceRenderer: Renderer { - var filePath: String { "Endpoints/Namespace.swift" } - var components: [[String]] - - init(endpoints: OpenAPIEndpoints) { - components = endpoints.filter(\.value.hasMethod).keys.map(makePathComponents) - } - - func render() throws -> String? { - let root = Tree(value: "root") - var current = root - - for comps in components { - let root = current - for c in comps { - current = current.child(c) - } - current = root - } - - func visit(_ tree: Tree) -> Decl { - EnumDecl(access: .public, name: tree.value, cases: [], - nested: tree.children.sorted().map(visit)) - } - - return """ - // autogenerated - - // swiftlint:disable all - import Foundation - - \(root.children.sorted().map { - SourceFile(decl: visit($0)).render() - }.joined(separator: "\n")) - - // swiftlint:enable all - - """.cleaned() - } -} - -// MARK: - private -private class Tree: Hashable, Comparable { - var value: String - var children: Set - - init(value: String) { - self.value = value - self.children = [] - } - - func child(_ value: String) -> Tree { - for child in children { - if child.value == value { - return child - } - } - return child(Tree(value: value)) - } - - func child(_ child: Tree) -> Tree { - children.insert(child) - return child - } - - static func == (lhs: Tree, rhs: Tree) -> Bool { - return lhs.value == rhs.value - && lhs.children == rhs.children - } - - static func < (lhs: Tree, rhs: Tree) -> Bool { - lhs.value < rhs.value - } - - func hash(into hasher: inout Hasher) { - hasher.combine(value) - hasher.combine(children) - } -} diff --git a/tools/Sources/AppStoreConnectGenForSwift/EndpointRenderer+DELETE.swift b/tools/Sources/AppStoreConnectGenForSwift/EndpointRenderer+DELETE.swift deleted file mode 100644 index f715ecb4..00000000 --- a/tools/Sources/AppStoreConnectGenForSwift/EndpointRenderer+DELETE.swift +++ /dev/null @@ -1,39 +0,0 @@ -import Foundation -import AppStoreConnectGenKit - -extension EndpointRenderer { - func declForDELETE(_ endpoint: OpenAPIEndpoint) -> Decl? { - guard let delete = endpoint.delete else { return nil } - - var methodDecl = declForMethod(delete, name: "DELETE", parameters: endpoint.parameters ?? []) - methodDecl.inheritances.append("Endpoint") - methodDecl.typealiases = [ - TypealiasDecl(access: .public, name: "Parameters", value: "Never") - ] + methodDecl.typealiases - - methodDecl.functions = [ - FunctionDecl( - access: .public, - name: "request", - arguments: [ - ArgumentDecl( - name: "baseURL", - alt: "with", - type: "URL" - ) - ], - parameterModifiers: [.throws], - returnType: "URLRequest?", - body: """ - var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) - components?.path = path - - var urlRequest = components?.url.map { URLRequest(url: $0) } - urlRequest?.httpMethod = "DELETE" - return urlRequest - """ - ) - ] + methodDecl.functions - return methodDecl - } -} diff --git a/tools/Sources/AppStoreConnectGenForSwift/EndpointRenderer+GET.swift b/tools/Sources/AppStoreConnectGenForSwift/EndpointRenderer+GET.swift deleted file mode 100644 index fcd05e98..00000000 --- a/tools/Sources/AppStoreConnectGenForSwift/EndpointRenderer+GET.swift +++ /dev/null @@ -1,317 +0,0 @@ -import Foundation -import AppStoreConnectGenKit - -extension EndpointRenderer { - func declForGET(_ endpoint: OpenAPIEndpoint) -> Decl? { - guard let get = endpoint.get else { return nil } - - var parametersDecl = StructDecl( - access: .public, - name: "Parameters", - inheritances: ["Hashable"], - members: [], - initializers: [], - functions: [] - ) - let params = buildParameters(get.content.parameters) - var queryItemComponents: [(key: String?, name: String, repr: Repr, required: Bool)] = [] - for (name, root, nested) in params { - if nested.isEmpty { - func customInitialValue(_ repr: Repr) -> String? { - if repr is ArrayRepr { - return "[]" - } else { - return nil - } - } - - let prop = root! - let repr = findRepr(for: prop.schema, with: name) - let type = repr.renderType(context: context) - parametersDecl.members.append(MemberDecl( - access: .public, - keyword: .var, - name: name, - type: "\(type.withRequired(prop.required))", - value: prop.required ?? false ? customInitialValue(repr).map { .assignment($0) } : nil, - doc: prop.description) - ) - if let nested = repr.buildDecl(context: context) { - parametersDecl.nested.append(nested) - } - queryItemComponents.append((nil, name, repr, prop.required ?? false)) - } else { - let type = TypeName(name) - var nested = declForNestedParameter(nested, for: name, with: context, - queryItemComponents: &queryItemComponents) - if let root = root { - let repr = findRepr(for: root.schema, with: "") - let type = repr.renderType(context: context).withRequired(root.required) - nested.subscripts = [ - SubscriptDecl( - access: .public, - arguments: [], - returnType: "\(type)", - getter: """ - self[Relation<\(type)>(key: "\(name)")] - """, - setter: """ - self[Relation<\(type)>(key: "\(name)")] = newValue - """ - ) - ] + nested.subscripts - queryItemComponents.append((name, name + "[]", repr, root.required ?? false)) - } - parametersDecl.members.append(MemberDecl( - access: .public, - keyword: .var, - name: name, - type: "\(type)", - value: .assignment("\(type)()"), - doc: root?.description) - ) - parametersDecl.nested.append(nested) - } - } - var methodDecl = declForMethod(get, name: "GET", parameters: endpoint.parameters ?? []) - methodDecl.inheritances.append("Endpoint") - methodDecl.members.append(MemberDecl( - access: .public, - keyword: .var, - name: "parameters", - type: "Parameters", - value: .assignment("Parameters()") - )) - methodDecl.extensions.append(parametersDecl) - func buildQueryItem(key: String? = nil, name: String, repr: Repr, required: Bool) -> String { - var value: String { - switch repr { - case is ArrayRepr: - return """ - parameters.\(name)\(required ? "" : "?").map { "\\($0)" }.joined(separator: ",") - """ - - default: - return """ - parameters.\(name).map { "\\($0)" } - """ - } - } - return """ - URLQueryItem(name: "\(key ?? name)", - value: \(value)) - """ - } - - methodDecl.functions = [ - FunctionDecl( - access: .public, - name: "request", - arguments: [ - ArgumentDecl( - name: "baseURL", - alt: "with", - type: "URL" - ) - ], - parameterModifiers: [.throws], - returnType: "URLRequest?", - body: """ - var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) - components?.path = path - - components?.queryItems = [ - \(queryItemComponents.map(buildQueryItem).joined(separator: ",\n").indent(to: 4)) - ].filter { $0.value != nil } - if components?.queryItems?.isEmpty ?? false { - components?.queryItems = nil - } - - var urlRequest = components?.url.map { URLRequest(url: $0) } - urlRequest?.httpMethod = "GET" - return urlRequest - """ - ) - ] + methodDecl.functions - return methodDecl - } -} - -private typealias ParameterPack = (name: String, - root: OpenAPIEndpoint.Parameter?, - nested: [OpenAPIEndpoint.Parameter]) - -private func buildParameters(_ parameters: [OpenAPIEndpoint.Parameter]?) -> [ParameterPack] { - var used: Set = [] - var parameters = parameters? - .filter { used.insert($0.name).inserted } - .sorted(by: { $0.name < $1.name }) ?? [] - - var result: [ParameterPack] = [] - while !parameters.isEmpty { - if let p = consumeParameter(¶meters) { - result.append(p) - } - } - - return result -} - -private func consumeParameter(_ parameters: inout [OpenAPIEndpoint.Parameter]) -> ParameterPack? { - let param = parameters.removeFirst() - let isNested = param.name.contains("[") - let name = isNested - ? param.name.components(separatedBy: "[")[0] - : param.name - - enum Match { - case exact - case prefix - } - func matchName(_ target: String) -> Match? { - if target == name { - return .exact - } - if target.hasPrefix(name + "[") { - return .prefix - } - return nil - } - - var root: OpenAPIEndpoint.Parameter? = isNested ? nil : param - var nested: [OpenAPIEndpoint.Parameter] = isNested ? [param] : [] - while let p = parameters.first, let match = matchName(p.name) { - parameters.removeFirst() - - switch match { - case .exact: - assert(root == nil) - root = p - - case .prefix: - nested.append(p) - } - } - - return (name, root, nested) -} - -private func declForNestedParameter( - _ nestedParameters: [OpenAPIEndpoint.Parameter], - for keyName: String, - with context: SwiftCodeBuilder.Context, - queryItemComponents: inout [(key: String?, name: String, repr: Repr, required: Bool)] -) -> StructDecl { - func makeKey(_ target: String) -> String { - let key = String(target.components(separatedBy: "[")[1].dropLast()) - return key.components(separatedBy: ".") - .map { $0.upperInitialLetter() } - .joined() - .lowerInitialLetter() - } - - func nestedRelationDecl(_ p: OpenAPIEndpoint.Parameter) -> Decl? { - let name = makeKey(p.name) - let repr = findRepr(for: p.schema, with: name) - return repr.buildDecl(context: context) - } - - func relationMemberDecl(_ p: OpenAPIEndpoint.Parameter) -> MemberDecl { - let name = makeKey(p.name) - let repr = findRepr(for: p.schema, with: name) - let innerType = repr.renderType(context: context) - let type = TypeName(rawValue: "Relation<\(innerType.withRequired(false))>") - let variable = Variable(key: name, type: type, - required: true, deprecated: p.schema.deprecated, - description: p.description) - - func doc() -> String? { - let required = p.required ?? false - return variable.description.map { - $0 + (required ? " **(required)**" : "") - } ?? (required ? "**(required)**" : nil) - } - - queryItemComponents.append((p.name, keyName + "[.\(variable.escapedKey)]", repr, false)) - - return MemberDecl( - access: .public, - modifier: .static, - keyword: .var, - name: variable.escapedKey, - type: "\(variable.type)", - value: .computed(""" - .init(key: "\(p.name)") - """), - doc: doc() - ) - } - - return StructDecl( - access: .public, - name: "\(TypeName(keyName))", - inheritances: ["Hashable"], - subscripts: [ - SubscriptDecl( - access: .public, - generics: "T: Hashable", - arguments: [ - ArgumentDecl( - name: "relation", - alt: "_", - type: "Relation" - ) - ], - returnType: "T", - getter: "values[relation]?.base as! T", - setter: "values[relation] = AnyHashable(newValue)" - ) - ].compactMap { $0 }, - members: [ - MemberDecl( - access: .private, - keyword: .var, - name: "values", - type: "[AnyHashable: AnyHashable]", - value: .assignment("[:]") - ) - ], - initializers: [], - functions: [], - nested: nestedParameters.compactMap(nestedRelationDecl) + [ - StructDecl( - access: .public, - name: "Relation", - inheritances: ["Hashable"], - subscripts: [ - ], - members: nestedParameters.map(relationMemberDecl) + [ - MemberDecl( - access: .internal, - keyword: .let, - name: "key", - type: "String" - ) - ], - initializers: [], - functions: [ - FunctionDecl( - access: .public, - name: "hash", - arguments: [ - ArgumentDecl( - name: "hasher", - alt: "into", - type: "inout Hasher" - ) - ], - body: "hasher.combine(key)" - ) - ], - nested: [], - extensions: [] - ) - ], - extensions: [] - ) -} diff --git a/tools/Sources/AppStoreConnectGenForSwift/EndpointRenderer+POST_PATCH.swift b/tools/Sources/AppStoreConnectGenForSwift/EndpointRenderer+POST_PATCH.swift deleted file mode 100644 index 50a8f04e..00000000 --- a/tools/Sources/AppStoreConnectGenForSwift/EndpointRenderer+POST_PATCH.swift +++ /dev/null @@ -1,112 +0,0 @@ -import Foundation -import AppStoreConnectGenKit - -extension EndpointRenderer { - func declForPOST(_ endpoint: OpenAPIEndpoint) -> Decl? { - guard let post = endpoint.post else { return nil } - return baseDecl(method: post, httpMethod: "POST", - requestBody: post.content.requestBody, - parameters: endpoint.parameters ?? []) - } - - func declForPATCH(_ endpoint: OpenAPIEndpoint) -> Decl? { - guard let patch = endpoint.patch else { return nil } - return baseDecl(method: patch, httpMethod: "PATCH", - requestBody: patch.content.requestBody, - parameters: endpoint.parameters ?? []) - } - - private func baseDecl( - method: OpenAPIEndpoint.RequestMethod, - httpMethod: String, - requestBody: OpenAPIEndpoint.RequestBody, - parameters: [OpenAPIEndpoint.Parameter] - ) -> Decl? { - let (contentType, content) = requestBody.content.first! - let repr = findRepr(for: content.schema, with: "parameters") - let required = requestBody.required ?? false - - let paramType = TypeName(rawValue: "Parameters") - - var methodDecl = declForMethod( - method, name: httpMethod, - parameters: endpoint.parameters ?? [], - extraArguments: [ - (repr: repr, - name: "parameters", - type: paramType, - required: required, - doc: nil) - ]) - methodDecl.inheritances.append("Endpoint") - - if let decl = repr.buildDecl(context: context) { - methodDecl.extensions.append(decl) - } else { - methodDecl.typealiases = [TypealiasDecl( - access: .public, - name: "Parameters", - value: "\(repr.renderType(context: context).withRequired(required))" - )] + methodDecl.typealiases - } - methodDecl.members.append(MemberDecl( - access: .public, - keyword: .var, - name: "parameters", - type: "\(paramType.withRequired(required))", - doc: requestBody.description - )) - methodDecl.functions = [ - requestFunctionDecl(httpMethod: httpMethod, contentType: contentType) - ] + methodDecl.functions - return methodDecl - } -} - -private func requestFunctionDecl(httpMethod: String, contentType: String) -> FunctionDecl { - var body = """ - var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) - components?.path = path - - var urlRequest = components?.url.map { URLRequest(url: $0) } - urlRequest?.httpMethod = "\(httpMethod)" - - - """ - - switch contentType { - case "application/json": - body += """ - var jsonEncoder: JSONEncoder { - let encoder = JSONEncoder() - return encoder - } - - urlRequest?.httpBody = try jsonEncoder.encode(parameters) - urlRequest?.setValue("\(contentType)", forHTTPHeaderField: "Content-Type") - - """ - - default: - fatalError("unsupported content type \(contentType)") - } - - body += """ - return urlRequest - """ - - return FunctionDecl( - access: .public, - name: "request", - arguments: [ - ArgumentDecl( - name: "baseURL", - alt: "with", - type: "URL" - ) - ], - parameterModifiers: [.throws], - returnType: "URLRequest?", - body: body - ) -} diff --git a/tools/Sources/AppStoreConnectGenForSwift/EndpointRenderer.swift b/tools/Sources/AppStoreConnectGenForSwift/EndpointRenderer.swift deleted file mode 100644 index b1243892..00000000 --- a/tools/Sources/AppStoreConnectGenForSwift/EndpointRenderer.swift +++ /dev/null @@ -1,334 +0,0 @@ -import Foundation -import CryptoKit -import AppStoreConnectGenKit - -struct EndpointRenderer: Renderer { - enum Method: String { - case GET, POST, PATCH, DELETE - } - var endpoint: OpenAPIEndpoint - var context: SwiftCodeBuilder.Context - var filePath: String - - let path: String - let pathComponents: [String] - let name: String - let method: Method - - static func generates( - path: String, - endpoint: OpenAPIEndpoint, - context: SwiftCodeBuilder.Context - ) -> [Self] { - return [ - .init(path: path, method: .GET, endpoint: endpoint, context: context), - .init(path: path, method: .POST, endpoint: endpoint, context: context), - .init(path: path, method: .PATCH, endpoint: endpoint, context: context), - .init(path: path, method: .DELETE, endpoint: endpoint, context: context) - ].compactMap { $0 } - } - - init?( - path: String, - method: Method, - endpoint: OpenAPIEndpoint, - context: SwiftCodeBuilder.Context - ) { - func tags(_ method: OpenAPIEndpoint.RequestMethod?) -> Set { - method?.tags ?? [] - } - let name = tags(endpoint.get) - .union(tags(endpoint.post)) - .union(tags(endpoint.patch)) - .union(tags(endpoint.delete)) - .sorted() - .first - guard let name = name else { return nil } - - let pathComponents = makePathComponents(from: path) - - func makeFileName() -> String { - var comps = pathComponents - while comps.first != name { - comps.removeFirst() - } - let uniqueHash = path.sha1?.prefix(7) - return "\(comps.joined()).\(method)\(uniqueHash.map { ".\($0)" } ?? "").generated.swift" - } - - self.path = path - self.pathComponents = pathComponents - self.name = name - self.method = method - self.endpoint = endpoint - self.context = context - self.filePath = "Endpoints/" - + pathComponents.joined(separator: "/") - + "/" - + makeFileName() - } - - func render() -> String? { - func _render(_ decl: Decl?) -> String? { - guard let decl = decl else { return nil } - - return """ - // autogenerated - - // swiftlint:disable all - import Foundation - #if canImport(FoundationNetworking) - import FoundationNetworking - #endif - - \(SourceFile(decl: ExtensionDecl( - name: pathComponents.joined(separator: "."), - body: [decl] - )).render()) - - // swiftlint:enable all - - """.cleaned() - } - - switch method { - case .GET: - return _render(declForGET(endpoint)) - - case .POST: - return _render(declForPOST(endpoint)) - - case .PATCH: - return _render(declForPATCH(endpoint)) - - case .DELETE: - return _render(declForDELETE(endpoint)) - } - } - - typealias ParameterPack = ( - repr: Repr, - name: String, - type: TypeName, - required: Bool, - doc: String? - ) - - func declForMethod(_ method: OpenAPIEndpoint.RequestMethod, - name: String, - parameters: [OpenAPIEndpoint.Parameter], - extraArguments: [ParameterPack] = []) -> StructDecl { - var results: [ParameterPack] = [] - for p in parameters { - let repr = findRepr(for: p.schema, with: p.name) - let type = repr.renderType(context: context) - results.append((repr, p.name, type, p.required ?? false, p.description)) - } - func declForQueryParameter(_ pack: ParameterPack) -> MemberDecl { - return MemberDecl( - access: .public, - keyword: .var, - name: pack.name, - type: "\(pack.type)", - doc: pack.doc - ) - } - func declForInitializerArgument(_ pack: ParameterPack) -> ArgumentDecl { - return ArgumentDecl( - name: pack.name, - type: "\(pack.type)", - initial: pack.required ? nil : "nil" - ) - } - - var decl = StructDecl( - access: .public, - name: name, - members: [], - initializers: [], - functions: [] - ) - - decl.annotations = method.deprecated ?? false ? [.deprecated()] : [] - decl.members.append( - MemberDecl( - access: .public, - keyword: .var, - name: "path", - type: "String", - value: .computed( - """ - "\(path - .components(separatedBy: "/") - .map { c in - c.hasPrefix("{") && c.hasSuffix("}") - ? #"\(\#(c.dropFirst().dropLast()))"# - : c - } - .joined(separator: "/"))" - """ - ) - ) - ) - decl.members.append(contentsOf: results.map(declForQueryParameter)) - decl.initializers.append( - InitializerDecl( - access: .public, - arguments: (results + extraArguments) - .map(declForInitializerArgument), - body: (results + extraArguments) - .map { "self.\($0.name) = \($0.name)" } - .joined(separator: "\n") - ) - ) - buildResponseDecl(into: &decl, method: method, context: context) - return decl - } -} - -private struct Doc { - var type: String? - var prefix: String? - var status: Int - var description: String? - - func render() -> String? { - if type == nil, prefix == nil, description == nil { - return nil - } - return [ - prefix.map { "- \($0)" }, - "**\(status)**\(description != nil ? "," : "")", - description, - type.map { "as `\($0)`" } - ].compactMap { $0 }.joined(separator: " ") - } -} - -private func buildResponseDecl(into baseDecl: inout StructDecl, - method: OpenAPIEndpoint.RequestMethod, - context: SwiftCodeBuilder.Context) { - var function = FunctionDecl( - access: .public, - declModifier: .static, - name: "response", - arguments: [ - ArgumentDecl( - name: "data", - alt: "from", - type: "Data" - ), - ArgumentDecl( - name: "urlResponse", - type: "HTTPURLResponse" - ) - ], - parameterModifiers: [.throws], - returnType: "Response", - body: "" - ) - var docs: [Doc] = [] - var body = """ - var jsonDecoder: JSONDecoder { - let decoder = JSONDecoder() - return decoder - } - - switch urlResponse.statusCode { - - """ - func render(success: Bool, contentType: String, content: OpenAPIEndpoint.Response.Content, doc: inout Doc) -> String { - let repr = findRepr(for: content.schema, with: "") - let type = repr.renderType(context: context) - doc.type = "\(type)" - if success { - assert(baseDecl.typealiases.isEmpty) - baseDecl.typealiases.append( - TypealiasDecl(access: .public, name: "Response", value: "\(type)") - ) - } - func renderReturnModifier() -> String { - success ? "return" : "throw" - } - - switch contentType { - case "application/json", - "application/vnd.apple.diagnostic-logs+json", - "application/vnd.apple.xcode-metrics+json": - return """ - \(success ? "return" : "throw") try jsonDecoder.decode(\(type).self, from: data) - """ - - case "text/csv": - return """ - return String(data: data, encoding: .utf8) ?? "" - """ - - case "gzip", "application/a-gzip": - assert(success) - return """ - return data - """ - - default: - fatalError("unsupported content type \(contentType)") - } - } - for (status, response) in method.responses.sorted(by: { $0.key < $1.key }) { - let isSuccess = (200..<300).contains(status) - let content: (contentType: String, content: OpenAPIEndpoint.Response.Content)? - - if response.content?.isEmpty ?? false { - assertionFailure(String(describing: response.content)) - continue - } else { - let found = (response.content?.first(where: { $0.key == "application/json" }) - ?? response.content?.first) - content = found.map { ($0.key, $0.value) } - } - - var doc = Doc(prefix: isSuccess ? "Returns:" : "Throws:", - status: status, - description: response.description) - defer { - docs.append(doc) - } - - guard let (contentType, content) = content else { - if isSuccess { - body += """ - case \(status): - return - - - """ - } - continue - } - body += """ - case \(status): - \(render(success: isSuccess, contentType: contentType, content: content, doc: &doc) - .trimmingCharacters(in: .whitespacesAndNewlines) - .indent(to: 4)) - - - """ - } - - body = body.trimmingCharacters(in: .whitespaces) + """ - default: - throw try jsonDecoder.decode(ErrorResponse.self, from: data) - } - """ - - if baseDecl.typealiases.isEmpty { - baseDecl.typealiases.append(TypealiasDecl(access: .public, name: "Response", value: "Void")) - } - - function.body = body - function.doc = { - let docs = docs.compactMap { $0.render() } - return docs.isEmpty ? nil : docs.joined(separator: "\n") - }() - baseDecl.functions.append(function) -} diff --git a/tools/Sources/AppStoreConnectGenForSwift/RendererSwiftCode.swift b/tools/Sources/AppStoreConnectGenForSwift/RendererSwiftCode.swift deleted file mode 100644 index 060a8fd0..00000000 --- a/tools/Sources/AppStoreConnectGenForSwift/RendererSwiftCode.swift +++ /dev/null @@ -1,75 +0,0 @@ -import Foundation -import AppStoreConnectGenKit - -public struct SwiftCodeBuilder { - var schemas: OpenAPISchemas = [:] - var endpoints: OpenAPIEndpoints = [:] - var context = Context() - - public init() {} - - public mutating func add(schemas newSchemas: OpenAPISchemas) { - schemas = newSchemas - - context.resolver = { ref in - newSchemas[ref.key] - } - } - - public mutating func add(endpoints newEndpoints: OpenAPIEndpoints) { - endpoints = newEndpoints - } - - public mutating func nest(_ key: String, in target: String) { - context.nestings[key] = target - } - - public mutating func inherit(_ key: String, to target: String) { - context.inherits[target, default: []].append(key) - } - - public func build() -> [Renderer] { - func schemaRenderers() -> [Renderer] { - schemas.map { - var path = "Schemas" - if $0.key.hasSuffix("Request") { - path += "/Requests" - } else if $0.key.hasSuffix("Response") { - path += "/Responses" - } - path += "/\($0.key).generated.swift" - return SchemaRenderer(schema: $0.value, scope: $0.key, context: context, filePath: path) - } - } - func endpointRenderers() -> [Renderer] { - endpoints.flatMap { - EndpointRenderer.generates(path: $0.key, endpoint: $0.value, context: context) - } - } - - return schemaRenderers() - + endpointRenderers() - + [EndpointNamespaceRenderer(endpoints: endpoints)] - } -} - -extension SwiftCodeBuilder { - struct Context { - var resolver: (OpenAPISchema.Ref) -> OpenAPISchema? = { _ in nil } - var nestings: [String: String] = [:] - var inherits: [String: [String]] = [:] - } -} - -func makePathComponents(from path: String) -> [String] { - func transformBraces(_ value: String) -> String { - guard value.hasPrefix("{"), value.hasSuffix("}") else { return value } - return "by" + String(value.dropFirst().dropLast()).upperInitialLetter() - } - - return path - .split(separator: "/", omittingEmptySubsequences: true) - .map(String.init) - .map(transformBraces) - .map { $0.upperInitialLetter() } -} diff --git a/tools/Sources/AppStoreConnectGenForSwift/SchemaRenderer.swift b/tools/Sources/AppStoreConnectGenForSwift/SchemaRenderer.swift deleted file mode 100644 index b5ecb8ce..00000000 --- a/tools/Sources/AppStoreConnectGenForSwift/SchemaRenderer.swift +++ /dev/null @@ -1,46 +0,0 @@ -import Foundation -import AppStoreConnectGenKit - -struct SchemaRenderer: Renderer { - var schema: OpenAPISchema - var scopeName: String - var context: SwiftCodeBuilder.Context - var filePath: String - - init( - schema: OpenAPISchema, - scope scopeName: String, - context: SwiftCodeBuilder.Context, - filePath: String - ) { - self.schema = schema - self.scopeName = scopeName - self.context = context - self.filePath = filePath - } - - func render() -> String? { - let repr = findRepr(for: schema, with: schema.title ?? scopeName) - guard let content = repr.buildDecl(context: context) else { return nil } - - let decl: Decl - if let nest = context.nestings[scopeName] { - let parent = TypeName(nest) - decl = ExtensionDecl(name: "\(parent)", body: [content]) - } else { - decl = content - } - - return """ - // autogenerated - - // swiftlint:disable all - import Foundation - - \(SourceFile(decl: decl).render()) - - // swiftlint:enable all - - """.cleaned() - } -} diff --git a/tools/Sources/AppStoreConnectGenForSwift/String+Utils.swift b/tools/Sources/AppStoreConnectGenForSwift/String+Utils.swift deleted file mode 100644 index 86db8080..00000000 --- a/tools/Sources/AppStoreConnectGenForSwift/String+Utils.swift +++ /dev/null @@ -1,68 +0,0 @@ -import Foundation -import CryptoKit - -extension String { - func camelcased() -> String { - isSnakecase - ? components(separatedBy: "_") - .map { $0.lowercased() } - .map { $0.capitalized } - .map { reservedNames[$0] ?? $0 } - .joined(separator: "") - : self - } - - func upperInitialLetter() -> String { - String(self[startIndex]).uppercased() + dropFirst() - } - - func lowerInitialLetter() -> String { - String(self[startIndex]).lowercased() + dropFirst() - } - - private var isSnakecase: Bool { - contains("_") || allSatisfy { - $0.isUppercase || $0 == "_" - } - } -} - -extension String { - func indent(to spaces: Int) -> String { - guard spaces > 0 else { return self } - let space = Array(repeating: " ", count: spaces) - .joined(separator: "") - return self - .components(separatedBy: "\n") - .map { $0.isEmpty ? "" : "\(space)\($0)" } - .joined(separator: "\n") - } -} - -extension String { - func cleaned() -> String { - self - .components(separatedBy: "\n") - .map { $0.allSatisfy(\.isWhitespace) ? "" : $0 } - .joined(separator: "\n") - } -} - -extension String { - var sha1: String? { - guard let data = data(using: .utf8) else { return nil } - return Insecure.SHA1 - .hash(data: data) - .prefix(Insecure.SHA1.byteCount) - .map { String(format: "%02hhx", $0) } - .joined() - } -} - -private let reservedNames: [String: String] = [ - "Os": "OS", - "Ios": "iOS", - "Macos": "MacOS", - "Tvos": "TvOS", - "Watchos": "WatchOS" -] diff --git a/tools/Sources/AppStoreConnectGenForSwift/Types.swift b/tools/Sources/AppStoreConnectGenForSwift/Types.swift deleted file mode 100644 index 39cabcd6..00000000 --- a/tools/Sources/AppStoreConnectGenForSwift/Types.swift +++ /dev/null @@ -1,502 +0,0 @@ -import Foundation -import AppStoreConnectGenKit - -struct SortKey: Equatable, Comparable { - let key: String - - static let highPriorities = [ - "id", - "type", - "attributes", - "relationships", - "width", - "height" - ] - - static func < (lhs: SortKey, rhs: SortKey) -> Bool { - let lhsIndex = highPriorities.firstIndex(of: lhs.key) - let rhsIndex = highPriorities.firstIndex(of: rhs.key) - if let lhs = lhsIndex { - if let rhs = rhsIndex { - return lhs < rhs - } - return true - } - if rhsIndex != nil { - return false - } - return lhs.key < rhs.key - } -} - -struct TypeName: RawRepresentable, Hashable, CustomStringConvertible { - let rawValue: String - var description: String { rawValue } - - init(rawValue: String) { - self.rawValue = keywords.contains(rawValue) - ? "`\(rawValue)`" - : rawValue - } - - init(_ key: String) { - let key = key.upperInitialLetter() - self.init(rawValue: key) - } - - func withRequired(_ flag: Bool?) -> TypeName { - TypeName(rawValue: rawValue + (flag == true ? "" : "?")) - } -} - -struct IdentifierName: RawRepresentable, Hashable, CustomStringConvertible { - let rawValue: String - var description: String { rawValue } - - init(rawValue: String) { - if !rawValue.contains("-") { - self.rawValue = rawValue - } else { - // special case for containing `-` - self.rawValue = rawValue.replacingOccurrences(of: "-", with: "_").lowercased() - } - } - - init(_ key: String) { - self.init(rawValue: key.lowerInitialLetter()) - } -} - -struct Variable { - let key: String - let type: TypeName - let required: Bool - let deprecated: Bool - let description: String? - let reserved: Bool - - var escapedKey: String { - reserved ? "`\(key)`" : key - } - - init(key: String, type: TypeName, required: Bool, deprecated: Bool, description: String?) { - self.key = key - self.type = type - self.required = required - self.deprecated = deprecated - self.description = description - self.reserved = keywords.contains(key) - } -} - -protocol Repr { - init?(_ schema: OpenAPISchema, for key: String) - - func renderType(context: SwiftCodeBuilder.Context) -> TypeName - func buildDecl(context: SwiftCodeBuilder.Context) -> Decl? -} - -extension Repr { - func buildDecl(context: SwiftCodeBuilder.Context) -> Decl? { nil } -} - -func findRepr(for prop: OpenAPISchema, with key: String) -> Repr { - let targets = [ - StructRepr.self, - EnumRepr.self, - ArrayRepr.self, - OneOfRepr.self, - AnyKeyRepr.self, - StringRepr.self, - BooleanRepr.self, - IntegerRepr.self, - FloatingRepr.self, - RefRepr.self, - UndefinedRepr.self - ] as [Repr.Type] - - return targets.lazy - .compactMap { $0.init(prop, for: key) } - .first ?? { - fatalError("missing \(key) : \(prop)") - }() -} - -struct StructRepr: Repr { - let properties: [String: OpenAPISchema] - let required: Set - let key: String - - init?(_ schema: OpenAPISchema, for key: String) { - guard case .object(let properties, let required) = schema.value else { - return nil - } - self.properties = properties - self.required = required - self.key = key - } - - func renderType(context: SwiftCodeBuilder.Context) -> TypeName { - TypeName(key) - } - - func buildDecl(context: SwiftCodeBuilder.Context) -> Decl? { - var result: [( - key: String, - variable: Variable, - decl: Decl? - )] = [] - for (key, value) in properties.sorted(by: { SortKey(key: $0.key) < SortKey(key: $1.key) }) { - let required = required.contains(key) - - let repr = findRepr(for: value, with: key) - let variable = Variable(key: key, type: repr.renderType(context: context), - required: required, deprecated: value.deprecated, - description: value.description) - - result.append((key, variable, repr.buildDecl(context: context))) - } - - let name = "\(renderType(context: context))" - - return StructDecl( - access: .public, - name: name, - inheritances: (context.inherits[name] ?? []) + ["Hashable", "Codable"], - members: declForProperties(from: result.map(\.variable)), - initializers: [declForInitializer(from: result.map(\.variable))], - functions: [], - nested: [ - declForCodingKeys(from: result.map { ($0.variable.escapedKey, $0.key) }) - ] + result.compactMap(\.decl), - extensions: [] - ) - } -} - -struct EnumRepr: Repr { - let cases: Set - let key: String - - init?(_ schema: OpenAPISchema, for key: String) { - guard case .enum(let cases) = schema.value else { - return nil - } - self.cases = cases - self.key = key - } - - func renderType(context: SwiftCodeBuilder.Context) -> TypeName { - TypeName(key) - } - - func buildDecl(context: SwiftCodeBuilder.Context) -> Decl? { - var duplicatedKeys: Set = [] - let caseValues: [(key: IdentifierName, raw: String)] = cases - .sorted(by: >) - .map { value in - var rawKey = value.camelcased() - if rawKey.hasPrefix("-") { - rawKey = String(rawKey.dropFirst()) + "Desc" - } - let key: IdentifierName - if duplicatedKeys.insert(rawKey.lowercased()).inserted { - key = IdentifierName(rawKey) - } else { - key = IdentifierName(rawValue: rawKey) - } - return (key, value) - } - .sorted(by: { $0.key.rawValue < $1.key.rawValue }) - - let name = "\(renderType(context: context))" - - // do not generate `unknown` for resource type - if key == "type", cases.count == 1 { - return EnumDecl( - access: .public, - name: name, - inheritances: (context.inherits[name] ?? []) + ["String", "Hashable", "Codable"], - cases: caseValues.map { - CaseDecl(name: $0.key.rawValue, value: $0.key.rawValue == $0.raw ? nil : .string($0.raw)) - } - ) - } else { - return EnumDecl( - access: .public, - name: name, - inheritances: (context.inherits[name] ?? []) + ["Hashable", "Codable", "RawRepresentable"], - cases: caseValues.map { CaseDecl(name: $0.key.rawValue) } + [ - CaseDecl(name: "unknown", value: .arguments([ - ArgumentDecl(name: "", type: "String") - ])) - ], - initializers: [ - InitializerDecl( - access: .public, - arguments: [ - ArgumentDecl(name: "rawValue", type: "String") - ], - body: """ - switch rawValue { - \(caseValues.map { - #"case "\#($0.raw)": self = .\#($0.key)"# - }.joined(separator: "\n")) - default: self = .unknown(rawValue) - } - """ - ) - ], - members: [ - MemberDecl( - access: .public, - keyword: .var, - name: "rawValue", - type: "String", - value: .computed(""" - switch self { - \(caseValues.map { - #"case .\#($0.key): return "\#($0.raw)""# - }.joined(separator: "\n")) - case .unknown(let rawValue): return rawValue - } - """) - ) - ] - ) - } - } -} - -struct ArrayRepr: Repr { - let repr: Repr - - init?(_ schema: OpenAPISchema, for key: String) { - guard case .array(let prop) = schema.value else { return nil } - self.repr = findRepr(for: prop, with: key) - } - - func renderType(context: SwiftCodeBuilder.Context) -> TypeName { - TypeName(rawValue: "[\(repr.renderType(context: context))]") - } - - func buildDecl(context: SwiftCodeBuilder.Context) -> Decl? { - repr.buildDecl(context: context) - } -} - -struct OneOfRepr: Repr { - let props: [OpenAPISchema] - let key: String - - init?(_ schema: OpenAPISchema, for key: String) { - guard case .oneOf(let props) = schema.value else { return nil } - self.props = props - self.key = key - } - - func renderType(context: SwiftCodeBuilder.Context) -> TypeName { - TypeName(key) - } - - func buildDecl(context: SwiftCodeBuilder.Context) -> Decl? { - var result: [( - key: IdentifierName, - type: TypeName, - decl: Decl? - )] = [] - var used: Set = [] - for prop in props { - let repr = findRepr(for: prop, with: "") - let type = repr.renderType(context: context) - guard used.insert(type).inserted else { continue } - result.append((IdentifierName(type.rawValue), type, repr.buildDecl(context: context))) - } - - return EnumDecl( - access: .public, - name: "\(renderType(context: context))", - inheritances: ["Hashable", "Codable"], - cases: result.map { - CaseDecl(name: $0.key.rawValue, value: .arguments([ArgumentDecl(name: "", type: "\($0.type)")])) - }, - initializers: [ - InitializerDecl( - access: .public, - arguments: [ - ArgumentDecl(name: "decoder", alt: "from", type: "Decoder") - ], - modifiers: [.throws], - body: """ - self = try { - var lastError: Error! - \(result.map { - """ - do { - return .\($0.key)(try \($0.type)(from: decoder)) - } catch { - lastError = error - } - """ - }.joined(separator: "\n")) - throw lastError - }() - """ - ) - ], - functions: [ - FunctionDecl( - access: .public, - name: "encode", - arguments: [ - ArgumentDecl(name: "encoder", alt: "to", type: "Encoder") - ], - parameterModifiers: [.throws], - body: """ - switch self { - \(result.map { - """ - case .\($0.key)(let value): - try value.encode(to: encoder) - """ - }.joined(separator: "\n\n")) - } - """ - ) - ], - nested: result.compactMap(\.decl), - extensions: [] - ) - } -} - -struct AnyKeyRepr: Repr { - let repr: Repr - - init?(_ schema: OpenAPISchema, for key: String) { - guard case .anyKey(let value) = schema.value else { return nil } - self.repr = findRepr(for: value, with: key) - } - - func renderType(context: SwiftCodeBuilder.Context) -> TypeName { - TypeName(rawValue: "[String: \(repr.renderType(context: context))]") - } -} - -struct StringRepr: Repr { - let format: OpenAPISchema.Property.StringFormat? - - init?(_ schema: OpenAPISchema, for key: String) { - guard case .string(let format) = schema.value else { return nil } - self.format = format - } - - func renderType(context: SwiftCodeBuilder.Context) -> TypeName { - TypeName(rawValue: { - switch format { - case .date, .dateTime: return "String" - case .uri, .uriReference: return "URL" - case .email, .number, .duration: return "String" - case .binary: return "Data" - case nil: return "String" - } - }()) - } -} - -struct BooleanRepr: Repr { - init?(_ schema: OpenAPISchema, for key: String) { - guard case .boolean = schema.value else { return nil } - } - - func renderType(context: SwiftCodeBuilder.Context) -> TypeName { - TypeName(rawValue: "Bool") - } -} - -struct IntegerRepr: Repr { - init?(_ schema: OpenAPISchema, for key: String) { - guard case .integer = schema.value else { return nil } - } - - func renderType(context: SwiftCodeBuilder.Context) -> TypeName { - TypeName(rawValue: "Int") - } -} - -struct FloatingRepr: Repr { - init?(_ schema: OpenAPISchema, for key: String) { - guard case .number = schema.value else { return nil } - } - - func renderType(context: SwiftCodeBuilder.Context) -> TypeName { - TypeName(rawValue: "Float") - } -} - -struct RefRepr: Repr { - let ref: OpenAPISchema.Ref - - init?(_ schema: OpenAPISchema, for key: String) { - guard case .ref(let ref) = schema.value else { return nil } - self.ref = ref - } - - func renderType(context: SwiftCodeBuilder.Context) -> TypeName { - if let schema = context.resolver(ref) { - return findRepr(for: schema, with: schema.title ?? ref.key).renderType(context: context) - } - return TypeName(ref.key) - } -} - -struct UndefinedRepr: Repr { - init?(_ schema: OpenAPISchema, for key: String) { - guard case .undefined = schema.value else { return nil } - } - - func renderType(context: SwiftCodeBuilder.Context) -> TypeName { - return TypeName("Data") - } -} - -private func declForProperties(from props: [Variable]) -> [MemberDecl] { - props.map { - MemberDecl( - annotations: $0.deprecated ? [.deprecated()] : [], - access: .public, - keyword: .var, - name: $0.escapedKey, - type: "\($0.type.withRequired($0.required))", - doc: $0.description - ) - } -} - -private func declForCodingKeys(from props: [(key: String, value: String)]) -> Decl { - EnumDecl( - access: .private, - name: "CodingKeys", - inheritances: ["String", "CodingKey"], - cases: props.map { key, value in - CaseDecl(name: key, value: key == value ? nil : .string(value)) - } - ) -} - -private func declForInitializer(from props: [Variable]) -> InitializerDecl { - InitializerDecl( - access: .public, - arguments: props.map { - ArgumentDecl( - name: $0.reserved ? "_\($0.key)" : $0.key, - alt: $0.reserved ? $0.key : nil, - type: "\($0.type.withRequired($0.required))", - initial: $0.required ? nil : "nil" - ) - }, - body: props - .map { "self.\($0.escapedKey) = \($0.reserved ? "_\($0.key)" : $0.key)" } - .joined(separator: "\n") - ) -} diff --git a/tools/Sources/AppStoreConnectGenForSwift/keywords.swift b/tools/Sources/AppStoreConnectGenForSwift/keywords.swift deleted file mode 100644 index b33b0cff..00000000 --- a/tools/Sources/AppStoreConnectGenForSwift/keywords.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation - -let keywords: Set = [ - "self", - "Type" -] diff --git a/tools/Sources/AppStoreConnectGenKit/Generator.swift b/tools/Sources/AppStoreConnectGenKit/Generator.swift deleted file mode 100644 index 46e05c6b..00000000 --- a/tools/Sources/AppStoreConnectGenKit/Generator.swift +++ /dev/null @@ -1,51 +0,0 @@ -import Foundation -import CryptoKit - -public struct Generator { - private let outputDir: URL - - public init( - outputDir: URL - ) { - self.outputDir = outputDir - } - - public struct GeneratedFile { - public var path: URL - public var content: String - - public func write() throws { - try content.write(to: path, atomically: true, encoding: .utf8) - } - } - - @discardableResult - public func generate(renderers: [Renderer], - dryrun: Bool = false) throws -> [GeneratedFile] { - var generated: [GeneratedFile] = [] - - for renderer in renderers { - guard let content = try renderer.render() else { - continue - } - let path = outputDir.appendingPathComponent(renderer.filePath) - generated.append(.init(path: path, content: content)) - } - - if !dryrun { - try? FileManager.default.removeItem(at: outputDir) - - var dirs: Set = [] - for file in generated { - if case let dir = file.path.deletingLastPathComponent(), !dirs.contains(dir) { - dirs.insert(dir) - try? FileManager.default.createDirectory(at: dir, - withIntermediateDirectories: true, - attributes: nil) - } - try file.write() - } - } - return generated - } -} diff --git a/tools/Sources/AppStoreConnectGenKit/OpenAPIEndpoint.swift b/tools/Sources/AppStoreConnectGenKit/OpenAPIEndpoint.swift deleted file mode 100644 index 63e16df4..00000000 --- a/tools/Sources/AppStoreConnectGenKit/OpenAPIEndpoint.swift +++ /dev/null @@ -1,92 +0,0 @@ -import Foundation - -public typealias OpenAPIEndpoints = [String: OpenAPIEndpoint] - -public struct OpenAPIEndpoint: Decodable { - public var get: RequestMethod? - public var post: RequestMethod? - public var patch: RequestMethod? - public var delete: RequestMethod? - public var parameters: [Parameter]? - - public var hasMethod: Bool { - return get != nil - || post != nil - || patch != nil - || delete != nil - } -} - -extension OpenAPIEndpoint { - public struct RequestMethod: Decodable { - public var tags: Set - public var deprecated: Bool? - public var responses: [Int: Response] - public var content: T - - private enum CodingKeys: String, CodingKey { - case tags, responses, deprecated - } - - public init(from decoder: Decoder) throws { - let contaienr = try decoder.container(keyedBy: CodingKeys.self) - tags = try contaienr.decode(Set.self, forKey: .tags) - responses = try contaienr.decode([Int: Response].self, forKey: .responses) - deprecated = try contaienr.decodeIfPresent(Bool.self, forKey: .deprecated) - content = try T(from: decoder) - } - } -} - -extension OpenAPIEndpoint { - public enum Operations { - public struct GET: Decodable { - public var parameters: [Parameter]? - } - - public struct POST: Decodable { - public var requestBody: RequestBody - } - - public struct PATCH: Decodable { - public var requestBody: RequestBody - } - - public struct DELETE: Decodable {} - } -} - -extension OpenAPIEndpoint { - public struct Parameter: Decodable { - public var name: String - public var `in`: Location - public var description: String? - public var schema: OpenAPISchema - public var required: Bool? - - public enum Location: String, Decodable { - case path, query - } - } - - public struct RequestBody: Decodable { - public var description: String? - public var content: [String: Content] - public var required: Bool? - - public struct Content: Decodable { - public var schema: OpenAPISchema - } - } -} - -extension OpenAPIEndpoint { - public struct Response: Decodable { - public var description: String? - public var content: [String: Content]? - - public struct Content: Decodable { - public var schema: OpenAPISchema - } - } -} diff --git a/tools/Sources/AppStoreConnectGenKit/OpenAPISchema.swift b/tools/Sources/AppStoreConnectGenKit/OpenAPISchema.swift deleted file mode 100644 index 38e65bfc..00000000 --- a/tools/Sources/AppStoreConnectGenKit/OpenAPISchema.swift +++ /dev/null @@ -1,148 +0,0 @@ -import Foundation - -public typealias OpenAPISchemas = [String: OpenAPISchema] - -public struct OpenAPISchema: Decodable { - public var title: String? - public var description: String? - public var value: Property - public var deprecated: Bool - - private enum CodingKeys: String, CodingKey { - case title, description, deprecated - } - - public init(title: String? = nil, description: String? = nil, value: Property, deprecated: Bool) { - self.title = title - self.description = description - self.value = value - self.deprecated = deprecated - } - - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - title = try c.decodeIfPresent(String.self, forKey: .title) - description = try c.decodeIfPresent(String.self, forKey: .description) - value = try Property(from: decoder) - deprecated = try c.decodeIfPresent(Bool.self, forKey: .deprecated) ?? false - } - - public func withDescription(_ description: String?) -> OpenAPISchema { - var copy = self - copy.description = description - return copy - } -} - -extension OpenAPISchema { - public indirect enum Property: Decodable { - case ref(Ref) - case object(properties: [String: OpenAPISchema], required: Set) - case array(OpenAPISchema) - case `enum`(Set) - case string(format: StringFormat?) - case integer - case number - case boolean - case anyKey(OpenAPISchema) - case oneOf([OpenAPISchema]) - case undefined - - public enum StringFormat: String, Decodable { - case email - case uri - case uriReference = "uri-reference" - case date - case dateTime = "date-time" - case number - case duration - case binary - } - - private enum _Type: String, Decodable { - case object - case array - case string - case integer - case number - case boolean - } - - private enum CodingKeys: String, CodingKey { - case type - // ref - case ref = "$ref" - // object - case properties - case required - - case additionalProperties - // array - case items - // enum - case `enum` - // string - case format - // oneOf - case oneOf - } - - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - if let ref = try c.decodeIfPresent(Ref.self, forKey: .ref) { - self = .ref(ref) - return - } - - switch try? c.decode(_Type.self, forKey: .type) { - case .object: - if let additionalProperties = try c.decodeIfPresent(OpenAPISchema.self, forKey: .additionalProperties) { - self = .anyKey(additionalProperties) - return - } - do { - let properties = try c.decode([String: OpenAPISchema].self, forKey: .properties) - let required = try c.decodeIfPresent(Set.self, forKey: .required) - self = .object(properties: properties, required: required ?? []) - } catch DecodingError.keyNotFound(CodingKeys.properties, _) { - self = .undefined - } - - case .array: - let value = try c.decode(OpenAPISchema.self, forKey: .items) - self = .array(value) - - case .string: - if let values = try c.decodeIfPresent(Set.self, forKey: .enum) { - self = .enum(values) - } else { - self = .string(format: try c.decodeIfPresent(StringFormat.self, forKey: .format)) - } - - case .integer: - self = .integer - - case .number: - self = .number - - case .boolean: - self = .boolean - - case nil: - let items = try c.decode([OpenAPISchema].self, forKey: .oneOf) - self = .oneOf(items) - } - } - } -} - -extension OpenAPISchema { - public struct Ref: RawRepresentable, Decodable { - public var rawValue: String - public var key: String { rawValue.components(separatedBy: "/").last! } - - public init(rawValue: String) { - self.rawValue = rawValue - } - } -} diff --git a/tools/Sources/AppStoreConnectGenKit/Renderer.swift b/tools/Sources/AppStoreConnectGenKit/Renderer.swift deleted file mode 100644 index 1ef3b806..00000000 --- a/tools/Sources/AppStoreConnectGenKit/Renderer.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation - -public protocol Renderer { - - var filePath: String { get } - - func render() throws -> String? -} diff --git a/tools/Sources/appstoreconnectgen/AppStoreConnectGen.swift b/tools/Sources/appstoreconnectgen/AppStoreConnectGen.swift deleted file mode 100644 index 4a818472..00000000 --- a/tools/Sources/appstoreconnectgen/AppStoreConnectGen.swift +++ /dev/null @@ -1,41 +0,0 @@ -import Foundation -import ArgumentParser -import AppStoreConnectGenKit -import AppStoreConnectGenForSwift - -@main -struct AppStoreConnectGen: ParsableCommand { - @Option(help: "path to open api file (json)", - completion: .file(extensions: ["json"])) - var openAPIPath: String - - @Option(name: .shortAndLong, - help: "path to output dir", - completion: .directory) - var output: String - - func run() throws { - let inputData = try Data(contentsOf: URL(fileURLWithPath: openAPIPath)) - let input = try JSONDecoder().decode(OpenAPIRoot.self, from: inputData) - - var builder = SwiftCodeBuilder() - - builder.add(schemas: input.components.schemas) - builder.add(endpoints: input.paths) - builder.nest("ErrorSourceParameter", in: "ErrorResponse") - builder.nest("ErrorSourcePointer", in: "ErrorResponse") - builder.inherit("Error", to: "ErrorResponse") - - let generator = Generator(outputDir: URL(fileURLWithPath: output)) - try generator.generate(renderers: builder.build()) - } -} - -struct OpenAPIRoot: Decodable { - struct Components: Decodable { - var schemas: OpenAPISchemas - } - - var components: Components - var paths: OpenAPIEndpoints -} diff --git a/tools/Tests/AppStoreConnectGenForSwiftTests/DeclTests.swift b/tools/Tests/AppStoreConnectGenForSwiftTests/DeclTests.swift deleted file mode 100644 index 1ea36048..00000000 --- a/tools/Tests/AppStoreConnectGenForSwiftTests/DeclTests.swift +++ /dev/null @@ -1,259 +0,0 @@ -import XCTest -@testable import AppStoreConnectGenForSwift - -private func render(_ decl: D) -> String { - SourceFile(decl: decl).render() -} - -class DeclTests: XCTestCase { - func testStruct() { - let expected = StructDecl( - access: .public, - name: "Foo", - inheritances: ["Hashable"], - subscripts: [ - SubscriptDecl( - access: .private, - arguments: [ - ArgumentDecl( - name: "bar", - type: "Int?", - initial: "nil" - ) - ], - returnType: "Int?", - getter: "nil" - ), - SubscriptDecl( - access: .public, - arguments: [], - returnType: "String", - getter: #""foobar""# - ), - SubscriptDecl( - access: .internal, - generics: "T: Hashable", - arguments: [ - ArgumentDecl( - name: "relation", alt: "_", type: "Relation" - ) - ], - returnType: "T", - getter: "fatalError()", - setter: "fatalError()" - ) - ], - members: [ - MemberDecl( - access: .public, - modifier: .static, - keyword: .var, - name: "staticFoo", - type: "String", - value: .computed(#""Hello world""#) - ), - MemberDecl( - access: .private, - keyword: .let, - name: "bar", - type: "Int", - value: .assignment("0") - ), - MemberDecl( - access: .private, - keyword: .var, - name: "baz", - type: "String", - doc: """ - this is doc comment. - this is doc comment. - """ - ) - ], - initializers: [ - InitializerDecl( - access: .public, - arguments: [ - ArgumentDecl( - name: "baz", - type: "String" - ) - ], - body: """ - self.baz = baz - """ - ) - ], - functions: [ - FunctionDecl( - access: .public, - name: "hash", - arguments: [ - ArgumentDecl( - name: "hasher", - alt: "into", - type: "inout Hasher" - ) - ], - returnType: nil, - body: """ - hasher.combine(baz) - """ - ) - ], - extensions: [ - EnumDecl( - access: .private, - name: "Enum", - cases: [ - CaseDecl( - name: "none" - ), - CaseDecl( - name: "value", - value: .arguments([ArgumentDecl(name: "value", type: "Int")]) - ) - ], - extensions: [ - EnumDecl( - access: .private, - name: "EnumNested1", - cases: [], - extensions: [ - EnumDecl( - access: .private, - name: "EnumNested2", - cases: [] - ) - ] - ) - ] - ) - ] - ) - XCTAssertEqual(render(expected), """ - public struct Foo: Hashable { - private subscript (bar: Int? = nil) -> Int? { - nil - } - - public subscript () -> String { - "foobar" - } - - internal subscript (_ relation: Relation) -> T { - get { fatalError() } - set { fatalError() } - } - - public static var staticFoo: String { - "Hello world" - } - - private let bar: Int = 0 - - /// this is doc comment. - /// this is doc comment. - private var baz: String - - public init(baz: String) { - self.baz = baz - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(baz) - } - } - - extension Foo { - private enum Enum { - case none - case value(value: Int) - } - } - - extension Foo.Enum { - private enum EnumNested1 {} - } - - extension Foo.Enum.EnumNested1 { - private enum EnumNested2 {} - } - """) - } - - func testExtension() { - let expected = render( - ExtensionDecl( - name: "Foo.Bar", - body: [ - FunctionDecl( - access: .public, - name: "f", - arguments: [], - returnType: "Int", - body: """ - return 100 - """ - ), - EnumDecl( - access: .private, - name: "Enum", - cases: [ - CaseDecl( - name: "none" - ), - CaseDecl( - name: "value", - value: .arguments([ArgumentDecl(name: "value", type: "Int")]) - ) - ], - nested: [ - EnumDecl( - access: .internal, - name: "InnerEnum", - cases: [], - extensions: [ - EnumDecl( - access: .private, - name: "EnumNested1", - cases: [], - extensions: [ - EnumDecl( - access: .private, - name: "EnumNested2", - cases: [] - ) - ] - ) - ] - ) - ] - ) - ] - ) - ) - XCTAssertEqual(expected, """ - extension Foo.Bar { - public func f() -> Int { - return 100 - } - - private enum Enum { - case none - case value(value: Int) - - internal enum InnerEnum {} - } - } - - extension Foo.Bar.Enum.InnerEnum { - private enum EnumNested1 {} - } - - extension Foo.Bar.Enum.InnerEnum.EnumNested1 { - private enum EnumNested2 {} - } - """) - } -}