├── Makefile ├── Sources ├── LexiconGenKit │ ├── Extensions │ │ └── StringExtensions.swift │ ├── Lexicon │ │ ├── LexiconDocument.swift │ │ ├── LexiconDocumentCollection.swift │ │ ├── LexiconReference.swift │ │ ├── ID.swift │ │ └── LexiconSchema.swift │ ├── Builder │ │ └── Builder.swift │ └── Generator │ │ ├── Generator.swift │ │ ├── GeneratorContext.swift │ │ └── Generator+Builders.swift └── LexiconGen │ └── LexiconGen.swift ├── .gitignore ├── Tests └── LexiconGenKitTests │ ├── Extensions │ └── StringExtensionsTests.swift │ ├── Builder │ └── BuilderTests.swift │ ├── Lexicon │ ├── LexiconReferenceTests.swift │ └── LexiconSchemeTests.swift │ └── Generator │ └── Generator+BuildersTests.swift ├── Package.resolved ├── .github └── workflows │ └── ci.yml ├── README.md ├── LICENSE.md └── Package.swift /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: 3 | @swift build 4 | 5 | .PHONY: test 6 | test: 7 | @swift test 8 | 9 | .PHONY: clean 10 | clean: 11 | @rm -rf \ 12 | ./.build 13 | -------------------------------------------------------------------------------- /Sources/LexiconGenKit/Extensions/StringExtensions.swift: -------------------------------------------------------------------------------- 1 | extension String { 2 | var headUppercased: String { 3 | prefix(1).uppercased() + dropFirst() 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Tests/LexiconGenKitTests/Extensions/StringExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import LexiconGenKit 4 | 5 | final class StringExtensionsTests: XCTestCase { 6 | func testHeadUppercased() throws { 7 | XCTAssertEqual("".headUppercased, "") 8 | XCTAssertEqual("abc".headUppercased, "Abc") 9 | XCTAssertEqual("ABC".headUppercased, "ABC") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-argument-parser", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-argument-parser.git", 7 | "state" : { 8 | "revision" : "9f39744e025c7d377987f30b03770805dcb0bcd1", 9 | "version" : "1.1.4" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-syntax", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-syntax.git", 16 | "state" : { 17 | "branch" : "509.0.0", 18 | "revision" : "74203046135342e4a4a627476dd6caf8b28fe11b" 19 | } 20 | } 21 | ], 22 | "version" : 2 23 | } 24 | -------------------------------------------------------------------------------- /Sources/LexiconGenKit/Lexicon/LexiconDocument.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct LexiconDocument: Decodable where Reference: LexiconReference { 4 | public var lexicon: Int 5 | public var id: NSID 6 | public var revision: Int? 7 | public var description: String? 8 | public var defs: [String: LexiconSchema] 9 | 10 | public func transformToAbsoluteReferences() throws -> LexiconDocument 11 | { 12 | try LexiconDocument( 13 | lexicon: lexicon, 14 | id: id, 15 | revision: revision, 16 | description: description, 17 | defs: defs.mapValues { try $0.transformToAbsoluteReference(nsid: id) } 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/LexiconGenKit/Lexicon/LexiconDocumentCollection.swift: -------------------------------------------------------------------------------- 1 | public class LexiconDocumentCollection where Reference: LexiconReference { 2 | public typealias Document = LexiconDocument 3 | 4 | public private(set) var docs = [Document]() 5 | 6 | public init() { 7 | } 8 | } 9 | 10 | public extension LexiconDocumentCollection { 11 | func add(_ doc: Document) { 12 | docs.append(doc) 13 | } 14 | 15 | func generateDefinitions() -> [LexiconDefinitionID: LexiconSchema] { 16 | var defs = [LexiconDefinitionID: LexiconSchema]() 17 | for doc in docs { 18 | defs.merge( 19 | doc.defs.map { (LexiconDefinitionID(nsid: doc.id, name: $0.key), $0.value) }, 20 | uniquingKeysWith: { $1 } 21 | ) 22 | } 23 | 24 | return defs 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/LexiconGenKitTests/Builder/BuilderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import LexiconGenKit 4 | 5 | final class BuilderTests: XCTestCase { 6 | func testUniqued() throws { 7 | XCTAssertEqual( 8 | Builder.uniqued([]), 9 | [] 10 | ) 11 | XCTAssertEqual( 12 | Builder.uniqued([ 13 | "Com.Example.Foo.Hoge", 14 | "Com.Example.Bar.Fuga", 15 | ]), 16 | [ 17 | "Hoge", 18 | "Fuga", 19 | ] 20 | ) 21 | XCTAssertEqual( 22 | Builder.uniqued([ 23 | "Com.Example.Foo.Hoge", 24 | "Com.Example.Bar.Fuga", 25 | "Com.Example.Baz.Fuga", 26 | ]), 27 | [ 28 | "Hoge", 29 | "BarFuga", 30 | "BazFuga", 31 | ] 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build: 8 | name: Build (${{ matrix.os }}, Swift ${{ matrix.swift-version }}) 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, macos-13] 13 | swift-version: ["5.9"] 14 | steps: 15 | - uses: swift-actions/setup-swift@v1 16 | with: 17 | swift-version: ${{ matrix.swift-version }} 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: make build 21 | 22 | test: 23 | name: Test (${{ matrix.os }}, Swift ${{ matrix.swift-version }}) 24 | runs-on: ${{ matrix.os }} 25 | strategy: 26 | matrix: 27 | os: [ubuntu-latest, macos-13] 28 | swift-version: ["5.9"] 29 | steps: 30 | - uses: swift-actions/setup-swift@v1 31 | with: 32 | swift-version: ${{ matrix.swift-version }} 33 | - uses: actions/checkout@v3 34 | - name: Test 35 | run: make test 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lexicon-gen ![CI](https://github.com/andooown/lexicon-gen/actions/workflows/ci.yml/badge.svg?branch=main) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fandooown%2Flexicon-gen%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/andooown/lexicon-gen) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fandooown%2Flexicon-gen%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/andooown/lexicon-gen) 2 | 3 | CLI tool written in Swift to generate Swift codes from [AT Proto's Lexicon](https://atproto.com/guides/lexicon) files. 4 | This tool is used to generate [swift-atproto](https://github.com/andooown/swift-atproto)'s `ATProtoAPI` library. 5 | 6 | 🚧 This package is under development. 🚧 7 | 8 | ## Requirements 9 | - Swift 5.9 or later 10 | 11 | ## Installation 12 | TBW 13 | 14 | ## Author 15 | - [andooown](https://github.com/andooown) 16 | 17 | ## License 18 | lexicon-gen is available under the MIT license. See the LICENSE file for more info. 19 | -------------------------------------------------------------------------------- /Tests/LexiconGenKitTests/Lexicon/LexiconReferenceTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import LexiconGenKit 4 | 5 | final class LexiconReferenceTests: XCTestCase { 6 | func testAbsoluteReference() throws { 7 | let inputs: [LexiconRelativeReference] = [ 8 | .init(rawValue: "object"), 9 | .init(rawValue: "#object"), 10 | .init(rawValue: "com.example.foo#object"), 11 | .init(rawValue: "com.example.foo"), 12 | ] 13 | 14 | let outputs: [LexiconAbsoluteReference] = try [ 15 | .init(rawValue: "com.example.root#object"), 16 | .init(rawValue: "com.example.root#object"), 17 | .init(rawValue: "com.example.foo#object"), 18 | .init(rawValue: "com.example.foo#main"), 19 | ] 20 | 21 | XCTAssertEqual(inputs.count, outputs.count) 22 | 23 | let nsid = try NSID("com.example.root") 24 | for (input, output) in zip(inputs, outputs) { 25 | XCTAssertEqual(try input.absoluteReference(nsid: nsid), output) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 andooown 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/LexiconGenKit/Builder/Builder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum Builder { 4 | public static func uniqued(_ fullNames: [String]) -> [String] { 5 | var segments = fullNames.map { $0.split(separator: ".").map(String.init) } 6 | 7 | var result = [String]() 8 | for i in segments.indices { 9 | var currentSegs = [String]() 10 | var segs = segments[i] 11 | while let seg = segs.popLast() { 12 | currentSegs.insert(seg, at: 0) 13 | 14 | let current = currentSegs.joined(separator: ".") 15 | let targets = result.enumerated().filter { $0.element == current }.map(\.offset) 16 | guard !targets.isEmpty else { 17 | break 18 | } 19 | 20 | for target in targets { 21 | var s = segments[target] 22 | result[target] = s.popLast()! + result[target] 23 | segments[target] = s 24 | } 25 | } 26 | 27 | segments[i] = segs 28 | result.append(currentSegs.joined()) 29 | } 30 | 31 | return result 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "lexicon-gen", 7 | platforms: [ 8 | .macOS(.v13), 9 | ], 10 | products: [ 11 | .executable( 12 | name: "lexicon-gen", 13 | targets: ["LexiconGen"] 14 | ), 15 | ], 16 | dependencies: [ 17 | .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"), 18 | .package(url: "https://github.com/apple/swift-syntax.git", revision: "509.0.0"), 19 | .package(url: "https://github.com/pointfreeco/swift-custom-dump.git", from: "1.0.0"), 20 | ], 21 | targets: [ 22 | .executableTarget( 23 | name: "LexiconGen", 24 | dependencies: [ 25 | .target(name: "LexiconGenKit"), 26 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 27 | ] 28 | ), 29 | .target( 30 | name: "LexiconGenKit", 31 | dependencies: [ 32 | .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), 33 | ] 34 | ), 35 | .testTarget( 36 | name: "LexiconGenKitTests", 37 | dependencies: [ 38 | .target(name: "LexiconGenKit"), 39 | .product(name: "CustomDump", package: "swift-custom-dump") 40 | ] 41 | ), 42 | ] 43 | ) 44 | -------------------------------------------------------------------------------- /Sources/LexiconGenKit/Generator/Generator.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxBuilder 3 | 4 | public struct Generator { 5 | private let context: GeneratorContext 6 | 7 | public init(context: GeneratorContext) { 8 | self.context = context 9 | } 10 | 11 | public func generate() throws -> String { 12 | let namespaces = context.generateNamespaceDefinitions() 13 | let definitions = context.generateDefinitions() 14 | 15 | let source = try SourceFileSyntax { 16 | try ImportDeclSyntax("import ATProtoCore") 17 | try ImportDeclSyntax("import ATProtoMacro") 18 | try ImportDeclSyntax("import ATProtoXRPC") 19 | try ImportDeclSyntax("import Foundation") 20 | 21 | try Generator.namespaces(namespaces) 22 | 23 | try Generator.unknownUnion(from: definitions) 24 | 25 | for definition in definitions { 26 | try ExtensionDeclSyntax( 27 | modifiers: [DeclModifierSyntax(name: .keyword(.public))], 28 | extendedType: TypeSyntax(stringLiteral: definition.parent) 29 | ) { 30 | try Generator.definition(definition) 31 | } 32 | } 33 | } 34 | 35 | let syntax = source.formatted() 36 | 37 | var generated = "" 38 | syntax.write(to: &generated) 39 | 40 | return generated 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/LexiconGenKit/Lexicon/LexiconReference.swift: -------------------------------------------------------------------------------- 1 | public protocol LexiconReference: Decodable, Equatable { 2 | init(rawValue: String) throws 3 | 4 | var rawValue: String { get } 5 | 6 | func absoluteReference(nsid: NSID) throws -> LexiconAbsoluteReference 7 | } 8 | 9 | public struct LexiconRelativeReference: LexiconReference { 10 | public let rawValue: String 11 | 12 | public init(rawValue: String) { 13 | self.rawValue = rawValue 14 | } 15 | 16 | public func absoluteReference(nsid: NSID) throws -> LexiconAbsoluteReference { 17 | if rawValue.hasPrefix("#") { 18 | return LexiconAbsoluteReference( 19 | LexiconDefinitionID(nsid: nsid, name: String(rawValue.dropFirst())) 20 | ) 21 | } else if rawValue.contains("#") { 22 | return LexiconAbsoluteReference(try LexiconDefinitionID(rawValue)) 23 | } else if rawValue.contains(".") { 24 | return LexiconAbsoluteReference( 25 | LexiconDefinitionID(nsid: try NSID(rawValue), name: "main") 26 | ) 27 | } else { 28 | return LexiconAbsoluteReference(LexiconDefinitionID(nsid: nsid, name: rawValue)) 29 | } 30 | } 31 | } 32 | 33 | public struct LexiconAbsoluteReference: LexiconReference { 34 | public let definitionID: LexiconDefinitionID 35 | 36 | public init(_ definitionID: LexiconDefinitionID) { 37 | self.definitionID = definitionID 38 | } 39 | 40 | public init(rawValue: String) throws { 41 | self.definitionID = try LexiconDefinitionID(rawValue) 42 | } 43 | 44 | public var rawValue: String { 45 | definitionID.value 46 | } 47 | 48 | public func absoluteReference(nsid: NSID) throws -> LexiconAbsoluteReference { 49 | self 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/LexiconGen/LexiconGen.swift: -------------------------------------------------------------------------------- 1 | import ArgumentParser 2 | import Foundation 3 | import LexiconGenKit 4 | import SwiftSyntax 5 | import SwiftSyntaxBuilder 6 | 7 | @main 8 | struct LexiconGen: ParsableCommand { 9 | @Option 10 | var sourceDirectory: String 11 | @Option 12 | var outputFile: String 13 | 14 | func run() throws { 15 | print("Source Directory = \(sourceDirectory)") 16 | print("Output File = \(outputFile)") 17 | 18 | let fileURLs = listJSONFiles(in: URL(fileURLWithPath: sourceDirectory, isDirectory: true)) 19 | 20 | print("\(fileURLs.count) files found") 21 | 22 | let context = try buildContext(from: fileURLs) 23 | 24 | print("\(context.generateNamespaceDefinitions().count) namespaces found") 25 | print("\(context.generateDefinitions().count) definitions found") 26 | 27 | print("Generating...") 28 | let start = Date() 29 | let generated = try Generator(context: context).generate() 30 | print("Completed in \(String(format: "%.3f", Date().timeIntervalSince(start))) s") 31 | 32 | let outputFileURL = URL(fileURLWithPath: outputFile) 33 | try generated.write(to: outputFileURL, atomically: true, encoding: .utf8) 34 | } 35 | } 36 | 37 | private extension LexiconGen { 38 | func listJSONFiles(in baseDirectory: URL) -> [URL] { 39 | guard 40 | let enumerator = FileManager.default.enumerator( 41 | at: baseDirectory, 42 | includingPropertiesForKeys: nil 43 | ) 44 | else { 45 | return [] 46 | } 47 | 48 | return enumerator.compactMap { $0 as? URL }.filter { 49 | $0.pathExtension.lowercased() == "json" 50 | } 51 | } 52 | 53 | func buildContext(from fileURLs: [URL]) throws -> GeneratorContext { 54 | let context = GeneratorContext() 55 | let decoder = JSONDecoder() 56 | for fileURL in fileURLs { 57 | let data = try Data(contentsOf: fileURL) 58 | let lex = try decoder.decode(LexiconDocument.self, from: data) 59 | 60 | context.append(try lex.transformToAbsoluteReferences()) 61 | } 62 | 63 | return context 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/LexiconGenKit/Lexicon/ID.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct NSID: Hashable, Decodable, CustomStringConvertible { 4 | public enum Error: Swift.Error { 5 | case lackOfParts 6 | } 7 | 8 | /// `com.example.foo` -> `["com", "example", "foo"]` 9 | public let segments: [String] 10 | 11 | public init(segments: [String]) { 12 | self.segments = segments 13 | } 14 | 15 | public init(_ rawValue: String) throws { 16 | let labels = rawValue.split(separator: ".").map(String.init) 17 | guard labels.count >= 3 else { 18 | throw Error.lackOfParts 19 | } 20 | 21 | self.segments = labels 22 | } 23 | 24 | public init(from decoder: Decoder) throws { 25 | let container = try decoder.singleValueContainer() 26 | let rawValue = try container.decode(String.self) 27 | 28 | do { 29 | try self.init(rawValue) 30 | } catch { 31 | throw DecodingError.dataCorrupted( 32 | DecodingError.Context( 33 | codingPath: decoder.codingPath, 34 | debugDescription: "error occurs during initializing from raw string" 35 | ) 36 | ) 37 | } 38 | } 39 | 40 | public var value: String { 41 | segments.joined(separator: ".") 42 | } 43 | 44 | public var description: String { 45 | value 46 | } 47 | } 48 | 49 | public struct LexiconDefinitionID: Hashable, Decodable, CustomStringConvertible { 50 | public enum Error: Swift.Error { 51 | case malformed 52 | } 53 | 54 | public let nsid: NSID 55 | public let name: String 56 | 57 | public init(nsid: NSID, name: String) { 58 | self.nsid = nsid 59 | self.name = name 60 | } 61 | 62 | public init(_ rawValue: String) throws { 63 | let components = rawValue.split(separator: "#").map(String.init) 64 | guard components.count == 2 else { 65 | throw Error.malformed 66 | } 67 | 68 | self.nsid = try NSID(components[0]) 69 | self.name = components[1] 70 | } 71 | 72 | public init(from decoder: Decoder) throws { 73 | let container = try decoder.singleValueContainer() 74 | let rawValue = try container.decode(String.self) 75 | 76 | do { 77 | try self.init(rawValue) 78 | } catch { 79 | throw DecodingError.dataCorrupted( 80 | DecodingError.Context( 81 | codingPath: decoder.codingPath, 82 | debugDescription: "error occurs during initializing from raw string" 83 | ) 84 | ) 85 | } 86 | } 87 | 88 | public var isMain: Bool { 89 | name == "main" 90 | } 91 | 92 | public var value: String { 93 | nsid.value + "#" + name 94 | } 95 | 96 | public var valueWithoutMain: String { 97 | nsid.value + (name == "main" ? "" : "#" + name) 98 | } 99 | 100 | public var description: String { 101 | value 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/LexiconGenKit/Generator/GeneratorContext.swift: -------------------------------------------------------------------------------- 1 | import LexiconGenKit 2 | 3 | class NamespaceNode: Equatable { 4 | let name: String 5 | private(set) var children = [NamespaceNode]() 6 | weak var parent: NamespaceNode? 7 | 8 | init(name: String) { 9 | self.name = name 10 | } 11 | 12 | var isRoot: Bool { 13 | self == .root 14 | } 15 | 16 | var allNodes: [NamespaceNode] { 17 | var nodes = isRoot ? [] : [self] 18 | for child in children { 19 | nodes.append(contentsOf: child.allNodes) 20 | } 21 | 22 | return nodes 23 | } 24 | 25 | var namespace: String { 26 | var names = [String]() 27 | 28 | var target = self 29 | while let n = target.parent { 30 | names.append(n.name) 31 | target = n 32 | } 33 | names.reverse() 34 | 35 | return names.filter { !$0.isEmpty }.joined(separator: ".") 36 | } 37 | 38 | var fullName: String { 39 | [namespace, name].filter { !$0.isEmpty }.joined(separator: ".") 40 | } 41 | 42 | func addNode(names: [String]) { 43 | guard !names.isEmpty else { 44 | return 45 | } 46 | 47 | var names = names 48 | let name = names.removeFirst() 49 | 50 | let target: NamespaceNode 51 | if let child = children.first(where: { $0.name == name }) { 52 | target = child 53 | } else { 54 | let child = NamespaceNode(name: name) 55 | child.parent = self 56 | children.append(child) 57 | 58 | target = child 59 | } 60 | 61 | target.addNode(names: names) 62 | } 63 | 64 | static var root: NamespaceNode { 65 | NamespaceNode(name: "") 66 | } 67 | 68 | static func == (lhs: NamespaceNode, rhs: NamespaceNode) -> Bool { 69 | lhs.name == rhs.name 70 | && lhs.children.count == rhs.children.count 71 | && zip(lhs.children, rhs.children).allSatisfy { $0 == $1 } 72 | && lhs.parent == rhs.parent 73 | } 74 | } 75 | 76 | public struct SwiftNamespaceDefinition { 77 | public let parent: String 78 | public let name: String 79 | 80 | public init(parent: String, name: String) { 81 | self.parent = parent 82 | self.name = name 83 | } 84 | 85 | public var fullName: String { 86 | parent + "." + name 87 | } 88 | } 89 | 90 | public struct SwiftDefinition { 91 | public let id: LexiconDefinitionID 92 | public let parent: String 93 | public let name: String 94 | public let object: Object 95 | 96 | public init( 97 | id: LexiconDefinitionID, 98 | parent: String, 99 | name: String, 100 | object: Object 101 | ) { 102 | self.id = id 103 | self.parent = parent 104 | self.name = name 105 | self.object = object 106 | } 107 | 108 | public var fullName: String { 109 | parent + "." + name 110 | } 111 | } 112 | 113 | public class GeneratorContext { 114 | private let docs = LexiconDocumentCollection() 115 | 116 | public init() {} 117 | 118 | public func append(_ doc: LexiconDocument) { 119 | docs.add(doc) 120 | } 121 | 122 | public func generateNamespaceDefinitions() -> [SwiftNamespaceDefinition] { 123 | let defs = generateDefinitions() 124 | 125 | let rootNode = NamespaceNode.root 126 | for def in defs { 127 | rootNode.addNode(names: def.parent.components(separatedBy: ".")) 128 | } 129 | 130 | let namespaces = Set(rootNode.allNodes.map(\.fullName).filter { !$0.isEmpty }) 131 | let definitions = Set(defs.map(\.fullName)) 132 | return namespaces.subtracting(definitions) 133 | .sorted() 134 | .compactMap { namespace in 135 | guard let (parent, name) = separateFullName(namespace) else { 136 | return nil 137 | } 138 | 139 | return SwiftNamespaceDefinition(parent: parent, name: name) 140 | } 141 | } 142 | 143 | public func generateDefinitions() -> [SwiftDefinition>] { 144 | docs.generateDefinitions() 145 | .sorted { $0.key.value < $1.key.value } 146 | .map { key, value in 147 | let (parent, name) = key.swiftDefinitionNames 148 | return SwiftDefinition(id: key, parent: parent, name: name, object: value) 149 | } 150 | } 151 | 152 | private func separateFullName(_ fullName: String) -> (parent: String, name: String)? { 153 | var components = fullName.components(separatedBy: ".") 154 | guard components.count >= 1 else { 155 | return nil 156 | } 157 | 158 | let name = components.removeLast() 159 | return (components.joined(separator: "."), name) 160 | } 161 | } 162 | 163 | public extension LexiconDefinitionID { 164 | var swiftDefinitionNames: (parent: String, name: String) { 165 | var namespaceComponents = nsid.segments.map(\.headUppercased) 166 | if isMain { 167 | let name = namespaceComponents.popLast()! 168 | let parent = namespaceComponents.joined(separator: ".") 169 | return (parent, name) 170 | } 171 | 172 | return ( 173 | namespaceComponents.joined(separator: "."), 174 | name.headUppercased 175 | ) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /Tests/LexiconGenKitTests/Lexicon/LexiconSchemeTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import LexiconGenKit 4 | 5 | final class LexiconSchemeTests: XCTestCase { 6 | func testTransformToAbsoluteReference() throws { 7 | let inputs: [LexiconSchema] = [ 8 | .null, 9 | .boolean, 10 | .integer, 11 | .string(format: "FORMAT"), 12 | .bytes, 13 | .cidLink, 14 | .blob, 15 | .array(.ref(.init(rawValue: "#type"))), 16 | .object( 17 | LexiconObjectSchema( 18 | properties: [ 19 | "a": .ref(.init(rawValue: "#typeA")), "b": .ref(.init(rawValue: "#typeB")), 20 | ], 21 | required: ["a"] 22 | ) 23 | ), 24 | .params, 25 | .token, 26 | .ref(.init(rawValue: "#ref")), 27 | .union([.init(rawValue: "#ref"), .init(rawValue: "com.namespace.absolute#ref")]), 28 | .unknown, 29 | .record( 30 | LexiconObjectSchema( 31 | properties: [ 32 | "a": .ref(.init(rawValue: "#typeA")), "b": .ref(.init(rawValue: "#typeB")), 33 | ], 34 | required: ["a"] 35 | ) 36 | ), 37 | .query( 38 | LexiconMethodSchema( 39 | parameters: LexiconObjectSchema( 40 | properties: [ 41 | "paramA": .ref(.init(rawValue: "#typeA")), 42 | "paramB": .ref(.init(rawValue: "#typeB")), 43 | ], 44 | required: ["paramA"] 45 | ), 46 | input: .object( 47 | LexiconObjectSchema( 48 | properties: [ 49 | "inputA": .ref(.init(rawValue: "#typeA")), 50 | "inputB": .ref(.init(rawValue: "#typeB")), 51 | ], 52 | required: ["inputA"] 53 | ) 54 | ), 55 | output: .object( 56 | LexiconObjectSchema( 57 | properties: [ 58 | "outputA": .ref(.init(rawValue: "#typeA")), 59 | "outputB": .ref(.init(rawValue: "#typeB")), 60 | ], 61 | required: ["outputA"] 62 | ) 63 | ) 64 | ) 65 | ), 66 | .procedure( 67 | LexiconMethodSchema( 68 | parameters: LexiconObjectSchema( 69 | properties: [ 70 | "paramA": .ref(.init(rawValue: "#typeA")), 71 | "paramB": .ref(.init(rawValue: "#typeB")), 72 | ], 73 | required: ["paramA"] 74 | ), 75 | input: .object( 76 | LexiconObjectSchema( 77 | properties: [ 78 | "inputA": .ref(.init(rawValue: "#typeA")), 79 | "inputB": .ref(.init(rawValue: "#typeB")), 80 | ], 81 | required: ["inputA"] 82 | ) 83 | ), 84 | output: .object( 85 | LexiconObjectSchema( 86 | properties: [ 87 | "outputA": .ref(.init(rawValue: "#typeA")), 88 | "outputB": .ref(.init(rawValue: "#typeB")), 89 | ], 90 | required: ["outputA"] 91 | ) 92 | ) 93 | ) 94 | ), 95 | .subscription, 96 | ] 97 | 98 | let outputs: [LexiconSchema] = try [ 99 | .null, 100 | .boolean, 101 | .integer, 102 | .string(format: "FORMAT"), 103 | .bytes, 104 | .cidLink, 105 | .blob, 106 | .array(.ref(.init(rawValue: "com.example.root#type"))), 107 | .object( 108 | LexiconObjectSchema( 109 | properties: [ 110 | "a": .ref(.init(rawValue: "com.example.root#typeA")), 111 | "b": .ref(.init(rawValue: "com.example.root#typeB")), 112 | ], 113 | required: ["a"] 114 | ) 115 | ), 116 | .params, 117 | .token, 118 | .ref(.init(rawValue: "com.example.root#ref")), 119 | .union([ 120 | .init(rawValue: "com.example.root#ref"), 121 | .init(rawValue: "com.namespace.absolute#ref"), 122 | ]), 123 | .unknown, 124 | .record( 125 | LexiconObjectSchema( 126 | properties: [ 127 | "a": .ref(.init(rawValue: "com.example.root#typeA")), 128 | "b": .ref(.init(rawValue: "com.example.root#typeB")), 129 | ], 130 | required: ["a"] 131 | ) 132 | ), 133 | .query( 134 | LexiconMethodSchema( 135 | parameters: LexiconObjectSchema( 136 | properties: [ 137 | "paramA": .ref(.init(rawValue: "com.example.root#typeA")), 138 | "paramB": .ref(.init(rawValue: "com.example.root#typeB")), 139 | ], 140 | required: ["paramA"] 141 | ), 142 | input: .object( 143 | LexiconObjectSchema( 144 | properties: [ 145 | "inputA": .ref(.init(rawValue: "com.example.root#typeA")), 146 | "inputB": .ref(.init(rawValue: "com.example.root#typeB")), 147 | ], 148 | required: ["inputA"] 149 | ) 150 | ), 151 | output: .object( 152 | LexiconObjectSchema( 153 | properties: [ 154 | "outputA": .ref(.init(rawValue: "com.example.root#typeA")), 155 | "outputB": .ref(.init(rawValue: "com.example.root#typeB")), 156 | ], 157 | required: ["outputA"] 158 | ) 159 | ) 160 | ) 161 | ), 162 | .procedure( 163 | LexiconMethodSchema( 164 | parameters: LexiconObjectSchema( 165 | properties: [ 166 | "paramA": .ref(.init(rawValue: "com.example.root#typeA")), 167 | "paramB": .ref(.init(rawValue: "com.example.root#typeB")), 168 | ], 169 | required: ["paramA"] 170 | ), 171 | input: .object( 172 | LexiconObjectSchema( 173 | properties: [ 174 | "inputA": .ref(.init(rawValue: "com.example.root#typeA")), 175 | "inputB": .ref(.init(rawValue: "com.example.root#typeB")), 176 | ], 177 | required: ["inputA"] 178 | ) 179 | ), 180 | output: .object( 181 | LexiconObjectSchema( 182 | properties: [ 183 | "outputA": .ref(.init(rawValue: "com.example.root#typeA")), 184 | "outputB": .ref(.init(rawValue: "com.example.root#typeB")), 185 | ], 186 | required: ["outputA"] 187 | ) 188 | ) 189 | ) 190 | ), 191 | .subscription, 192 | ] 193 | 194 | XCTAssertEqual(inputs.count, outputs.count) 195 | 196 | let nsid = try NSID("com.example.root") 197 | for (input, output) in zip(inputs, outputs) { 198 | XCTAssertEqual(try input.transformToAbsoluteReference(nsid: nsid), output) 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /Sources/LexiconGenKit/Lexicon/LexiconSchema.swift: -------------------------------------------------------------------------------- 1 | public struct LexiconObjectSchema: Decodable, Equatable 2 | where Reference: LexiconReference { 3 | public let properties: [String: LexiconSchema] 4 | public let required: [String]? 5 | 6 | public init(properties: [String: LexiconSchema], required: [String]?) { 7 | self.properties = properties 8 | self.required = required 9 | } 10 | 11 | public func transformToAbsoluteReference(nsid: NSID) throws -> LexiconObjectSchema< 12 | LexiconAbsoluteReference 13 | > { 14 | try LexiconObjectSchema( 15 | properties: properties.mapValues { 16 | try $0.transformToAbsoluteReference(nsid: nsid) 17 | }, 18 | required: required 19 | ) 20 | } 21 | } 22 | 23 | public struct LexiconMethodSchema: Decodable, Equatable 24 | where Reference: LexiconReference { 25 | private enum CodingKeys: String, CodingKey { 26 | case parameters 27 | case input 28 | case output 29 | } 30 | 31 | private enum ObjectCodingKeys: String, CodingKey { 32 | case schema 33 | } 34 | 35 | public let parameters: LexiconObjectSchema? 36 | public let input: LexiconSchema? 37 | public let output: LexiconSchema? 38 | 39 | public init( 40 | parameters: LexiconObjectSchema?, 41 | input: LexiconSchema?, 42 | output: LexiconSchema? 43 | ) { 44 | self.parameters = parameters 45 | self.input = input 46 | self.output = output 47 | } 48 | 49 | public init(from decoder: Decoder) throws { 50 | let container = try decoder.container(keyedBy: CodingKeys.self) 51 | 52 | parameters = try container.decodeIfPresent( 53 | LexiconObjectSchema.self, 54 | forKey: .parameters 55 | ) 56 | 57 | if container.contains(.input) { 58 | let inputContainer = try container.nestedContainer( 59 | keyedBy: ObjectCodingKeys.self, 60 | forKey: .input 61 | ) 62 | input = try inputContainer.decodeIfPresent( 63 | LexiconSchema.self, 64 | forKey: .schema 65 | ) 66 | } else { 67 | input = nil 68 | } 69 | 70 | if container.contains(.output) { 71 | let outputContainer = try container.nestedContainer( 72 | keyedBy: ObjectCodingKeys.self, 73 | forKey: .output 74 | ) 75 | // NOTE: `schema` is marked as required but `com.atproto.sync.getBlob` is not compliant it. 76 | output = try outputContainer.decodeIfPresent( 77 | LexiconSchema.self, 78 | forKey: .schema 79 | ) 80 | } else { 81 | output = nil 82 | } 83 | 84 | } 85 | 86 | public func transformToAbsoluteReference(nsid: NSID) throws -> LexiconMethodSchema< 87 | LexiconAbsoluteReference 88 | > { 89 | try LexiconMethodSchema( 90 | parameters: parameters?.transformToAbsoluteReference(nsid: nsid), 91 | input: input?.transformToAbsoluteReference(nsid: nsid), 92 | output: output?.transformToAbsoluteReference(nsid: nsid) 93 | ) 94 | } 95 | } 96 | 97 | public enum LexiconSchema: Decodable, Equatable where Reference: LexiconReference { 98 | case null 99 | case boolean 100 | case integer 101 | case string(format: String?) 102 | case bytes 103 | case cidLink 104 | case blob 105 | indirect case array(LexiconSchema) 106 | case object(LexiconObjectSchema) 107 | case params // TODO 108 | case token // TODO 109 | case ref(Reference) // TODO 110 | case union([Reference]) // TODO 111 | case unknown 112 | case record(LexiconObjectSchema) 113 | indirect case query(LexiconMethodSchema) 114 | indirect case procedure(LexiconMethodSchema) 115 | case subscription //TODO 116 | 117 | private enum TypeCodingKeys: String, CodingKey { 118 | case type 119 | } 120 | 121 | private enum LexiconType: String, Decodable { 122 | case null 123 | case boolean 124 | case integer 125 | case string 126 | case bytes 127 | case cidLink = "cid-link" 128 | case blob 129 | case array 130 | case object 131 | case params 132 | case token 133 | case ref 134 | case union 135 | case unknown 136 | case record 137 | case query 138 | case procedure 139 | case subscription 140 | } 141 | 142 | private enum StringTypeCodingKeys: String, CodingKey { 143 | case format 144 | } 145 | 146 | private enum ArrayTypeCodingKeys: String, CodingKey { 147 | case items 148 | } 149 | 150 | private enum ObjectTypeCodingKeys: String, CodingKey { 151 | case properties 152 | case required 153 | } 154 | 155 | private enum RefTypeCodingKeys: String, CodingKey { 156 | case ref 157 | } 158 | 159 | private enum UnionTypeCodingKeys: String, CodingKey { 160 | case refs 161 | } 162 | 163 | private enum RecordTypeCodingKeys: String, CodingKey { 164 | case record 165 | } 166 | 167 | public init(from decoder: Decoder) throws { 168 | let typeContainer = try decoder.container(keyedBy: TypeCodingKeys.self) 169 | let type = try typeContainer.decode(LexiconType.self, forKey: .type) 170 | 171 | switch type { 172 | case .null: 173 | self = .null 174 | 175 | case .boolean: 176 | self = .boolean 177 | 178 | case .integer: 179 | self = .integer 180 | 181 | case .string: 182 | let container = try decoder.container(keyedBy: StringTypeCodingKeys.self) 183 | let format = try container.decodeIfPresent(String.self, forKey: .format) 184 | self = .string(format: format) 185 | 186 | case .bytes: 187 | self = .bytes 188 | 189 | case .cidLink: 190 | self = .cidLink 191 | 192 | case .blob: 193 | self = .blob 194 | 195 | case .array: 196 | let container = try decoder.container(keyedBy: ArrayTypeCodingKeys.self) 197 | let items = try container.decode(LexiconSchema.self, forKey: .items) 198 | self = .array(items) 199 | 200 | case .object: 201 | let object = try LexiconObjectSchema(from: decoder) 202 | self = .object(object) 203 | 204 | case .params: 205 | self = .params 206 | 207 | case .token: 208 | self = .token 209 | 210 | case .ref: 211 | let container = try decoder.container(keyedBy: RefTypeCodingKeys.self) 212 | let id = try container.decode(String.self, forKey: .ref) 213 | self = .ref(try Reference(rawValue: id)) 214 | 215 | case .union: 216 | let container = try decoder.container(keyedBy: UnionTypeCodingKeys.self) 217 | let refs = try container.decode([String].self, forKey: .refs) 218 | self = try .union(refs.map { try Reference(rawValue: $0) }) 219 | 220 | case .unknown: 221 | self = .unknown 222 | 223 | case .record: 224 | let container = try decoder.container(keyedBy: RecordTypeCodingKeys.self) 225 | let object = try container.decode(LexiconObjectSchema.self, forKey: .record) 226 | self = .record(object) 227 | 228 | case .query: 229 | let method = try LexiconMethodSchema(from: decoder) 230 | self = .query(method) 231 | 232 | case .procedure: 233 | let method = try LexiconMethodSchema(from: decoder) 234 | self = .procedure(method) 235 | 236 | case .subscription: 237 | self = .subscription 238 | } 239 | } 240 | } 241 | 242 | public extension LexiconSchema { 243 | var isNull: Bool { 244 | guard case .null = self else { 245 | return false 246 | } 247 | return true 248 | } 249 | 250 | var isObject: Bool { 251 | guard case .object = self else { 252 | return false 253 | } 254 | return true 255 | } 256 | 257 | var isRecord: Bool { 258 | guard case .record = self else { 259 | return false 260 | } 261 | return true 262 | } 263 | 264 | var isQuery: Bool { 265 | guard case .query = self else { 266 | return false 267 | } 268 | return true 269 | } 270 | 271 | func transformToAbsoluteReference(nsid: NSID) throws -> LexiconSchema 272 | { 273 | switch self { 274 | case .null: 275 | return .null 276 | 277 | case .boolean: 278 | return .boolean 279 | 280 | case .integer: 281 | return .integer 282 | 283 | case .string(let format): 284 | return .string(format: format) 285 | 286 | case .bytes: 287 | return .bytes 288 | 289 | case .cidLink: 290 | return .cidLink 291 | 292 | case .blob: 293 | return .blob 294 | 295 | case .array(let schema): 296 | return .array(try schema.transformToAbsoluteReference(nsid: nsid)) 297 | 298 | case .object(let object): 299 | return .object(try object.transformToAbsoluteReference(nsid: nsid)) 300 | 301 | case .params: 302 | return .params 303 | 304 | case .token: 305 | return .token 306 | 307 | case .ref(let ref): 308 | return .ref(try ref.absoluteReference(nsid: nsid)) 309 | 310 | case .union(let refs): 311 | return try .union(refs.map { try $0.absoluteReference(nsid: nsid) }) 312 | 313 | case .unknown: 314 | return .unknown 315 | 316 | case .record(let object): 317 | return .record(try object.transformToAbsoluteReference(nsid: nsid)) 318 | 319 | case .query(let method): 320 | return .query(try method.transformToAbsoluteReference(nsid: nsid)) 321 | 322 | case .procedure(let method): 323 | return .procedure(try method.transformToAbsoluteReference(nsid: nsid)) 324 | 325 | case .subscription: 326 | return .subscription 327 | } 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /Sources/LexiconGenKit/Generator/Generator+Builders.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxBuilder 3 | 4 | public extension Generator { 5 | @CodeBlockItemListBuilder 6 | static func namespaces(_ namespaces: [SwiftNamespaceDefinition]) throws -> CodeBlockItemListSyntax { 7 | for namespace in namespaces { 8 | if namespace.parent.isEmpty { 9 | try EnumDeclSyntax("public enum \(raw: namespace.name)") {} 10 | } else { 11 | try ExtensionDeclSyntax("public extension \(raw: namespace.parent)") { 12 | try EnumDeclSyntax("enum \(raw: namespace.name)") {} 13 | } 14 | } 15 | } 16 | } 17 | 18 | @CodeBlockItemListBuilder 19 | static func unknownUnion(from definitions: [SwiftDefinition>]) throws -> CodeBlockItemListSyntax { 20 | let records = definitions.filter(\.object.isRecord) 21 | 22 | // public typealias LexiconUnknownUnion = Union2 23 | try TypeAliasDeclSyntax( 24 | "public typealias LexiconUnknownUnion = Union\(raw: records.count)<\(raw: records.map(\.fullName).joined(separator: ", "))>" 25 | ) 26 | 27 | // public extension LexiconUnknownUnion { 28 | // var asRecordA: App.Bsky.Foo.RecordA? { 29 | // asType0 30 | // } 31 | // var asRecordB: App.Bsky.Foo.RecordB? { 32 | // asType1 33 | // } 34 | // } 35 | try ExtensionDeclSyntax("public extension LexiconUnknownUnion") { 36 | let uniqued = Builder.uniqued(records.map(\.fullName)) 37 | for (i, (record, unique)) in zip(records, uniqued).enumerated() { 38 | try VariableDeclSyntax( 39 | """ 40 | var as\(raw: unique): \(raw: record.fullName)? { 41 | asType\(raw: i) 42 | } 43 | """ 44 | ) 45 | } 46 | } 47 | } 48 | 49 | @MemberBlockItemListBuilder 50 | static func definition(_ definition: SwiftDefinition>) throws -> MemberBlockItemListSyntax { 51 | switch definition.object { 52 | case .null: 53 | Generator.emptySyntax() 54 | 55 | case .boolean, 56 | .integer, 57 | .string, 58 | .array, 59 | .union: 60 | if let typeName = Generator.swiftTypeName(for: definition.object) { 61 | try TypeAliasDeclSyntax("typealias \(raw: definition.name) = \(raw: typeName)") 62 | } 63 | 64 | case .object(let object), 65 | .record(let object): 66 | try Generator.objectSyntax( 67 | name: definition.name, 68 | inheritances: ["UnionCodable", "Hashable"], 69 | object: object 70 | ) { 71 | try VariableDeclSyntax( 72 | "public static let typeValue = #LexiconDefID(\"\(raw: definition.id.valueWithoutMain)\")" 73 | ) 74 | } 75 | 76 | case .query(let method), 77 | .procedure(let method): 78 | try StructDeclSyntax("struct \(raw: definition.name): XRPCRequest") { 79 | // Parameters 80 | if let parameters = method.parameters { 81 | try objectSyntax( 82 | name: "Parameters", 83 | modifiers: ["public"], 84 | inheritances: ["XRPCRequestParametersConvertible"], 85 | object: parameters 86 | ) { 87 | let params = parameters.properties 88 | .sorted { $0.key < $1.key } 89 | .compactMap { k, v -> String? in 90 | guard Generator.swiftTypeName(for: v) != nil else { 91 | return nil 92 | } 93 | 94 | return "parameters.append(contentsOf: \(k).toQueryItems(name: \"\(k)\"))" 95 | } 96 | 97 | if params.isEmpty { 98 | try VariableDeclSyntax( 99 | """ 100 | public let queryItems: [URLQueryItem] = [] 101 | """ 102 | ) 103 | } else { 104 | try VariableDeclSyntax( 105 | """ 106 | public var queryItems: [URLQueryItem] { 107 | var parameters = [URLQueryItem]() 108 | \(raw: params.joined(separator: "\n")) 109 | 110 | return parameters 111 | } 112 | """ 113 | ) 114 | } 115 | } 116 | } 117 | 118 | // Input 119 | switch method.input { 120 | case .object(let object): 121 | try objectSyntax( 122 | name: "Input", 123 | modifiers: ["public"], 124 | inheritances: ["Encodable"], 125 | object: object 126 | ) 127 | 128 | default: 129 | Generator.emptySyntax() 130 | } 131 | 132 | // Output 133 | switch method.output { 134 | case .object(let object): 135 | try objectSyntax( 136 | name: "Output", 137 | modifiers: ["public"], 138 | inheritances: ["Decodable", "Hashable"], 139 | object: object 140 | ) 141 | 142 | case .ref(let ref): 143 | if let type = Generator.swiftTypeName(for: .ref(ref)) { 144 | try TypeAliasDeclSyntax("public typealias Output = \(raw: type)") 145 | } 146 | 147 | default: 148 | Generator.emptySyntax() 149 | } 150 | 151 | // Initializer 152 | Generator.requestInitializerSyntax( 153 | parameters: method.parameters, 154 | input: method.input 155 | ) 156 | 157 | let requestType = definition.object.isQuery ? "query" : "procedure" 158 | try VariableDeclSyntax( 159 | "public let type = XRPCRequestType.\(raw: requestType)" 160 | ) 161 | 162 | try VariableDeclSyntax( 163 | "public let requestIdentifier = \"\(raw: definition.id.nsid)\"" 164 | ) 165 | 166 | if method.parameters != nil { 167 | try VariableDeclSyntax( 168 | "public let parameters: Parameters" 169 | ) 170 | } 171 | if method.input != nil { 172 | try VariableDeclSyntax( 173 | "public let input: Input?" 174 | ) 175 | } 176 | } 177 | 178 | case .subscription: 179 | try EnumDeclSyntax("enum \(raw: definition.name)") {} 180 | 181 | default: 182 | Generator.emptySyntax() 183 | } 184 | } 185 | 186 | @MemberBlockItemListBuilder 187 | static func objectSyntax( 188 | name: String, 189 | modifiers: [String] = [], 190 | inheritances: [String] = [], 191 | object: LexiconObjectSchema, 192 | @MemberBlockItemListBuilder additionalBody: () throws -> MemberBlockItemListSyntax = { MemberBlockItemListSyntax([]) } 193 | ) throws -> MemberBlockItemListSyntax { 194 | let modifier = modifiers.isEmpty ? "" : modifiers.joined(separator: " ") + " " 195 | let inherit = inheritances.isEmpty ? "" : ": " + inheritances.joined(separator: ", ") 196 | 197 | try StructDeclSyntax("\(raw: modifier)struct \(raw: name)\(raw: inherit)") { 198 | try Generator.objectPropertiesSyntax(object.properties, required: object.required) 199 | Generator.objectInitializerSyntax(object.properties, required: object.required) 200 | 201 | try additionalBody() 202 | } 203 | } 204 | } 205 | 206 | private extension Generator { 207 | static func swiftTypeName(for scheme: LexiconSchema) -> String? { 208 | switch scheme { 209 | case .null: 210 | return nil 211 | 212 | case .boolean: 213 | return "Bool" 214 | 215 | case .integer: 216 | return "Int" 217 | 218 | case .string(format: "at-uri"): 219 | return "ATURI" 220 | 221 | case .string(format: "datetime"): 222 | return "Date" 223 | 224 | case .string(format: "uri"): 225 | return "SafeURL" 226 | 227 | case .string: 228 | return "String" 229 | 230 | case .bytes: 231 | return nil 232 | 233 | case .cidLink: 234 | return nil 235 | 236 | case .blob: 237 | return nil 238 | 239 | case .array(let element): 240 | return swiftTypeName(for: element).map { "[" + $0 + "]" } 241 | 242 | case .object: 243 | return nil 244 | 245 | case .params: 246 | return nil 247 | 248 | case .token: 249 | return nil 250 | 251 | case .ref(let ref): 252 | if ref.rawValue.contains("#") { 253 | let (parent, name) = ref.definitionID.swiftDefinitionNames 254 | return parent + "." + name 255 | } else { 256 | return ref.rawValue.split(separator: ".").map(String.init).map(\.headUppercased) 257 | .joined( 258 | separator: "." 259 | ) 260 | } 261 | 262 | case .union(let refs): 263 | let types = refs.compactMap { swiftTypeName(for: .ref($0)) } 264 | guard !types.isEmpty else { 265 | return nil 266 | } 267 | return "Union\(types.count)<\(types.joined(separator: ", "))>" 268 | 269 | case .unknown: 270 | return "LexiconUnknownUnion" 271 | 272 | case .record: 273 | return nil 274 | 275 | case .query: 276 | return nil 277 | 278 | case .procedure: 279 | return nil 280 | 281 | case .subscription: 282 | return nil 283 | } 284 | } 285 | 286 | @MemberBlockItemListBuilder 287 | static func objectPropertiesSyntax( 288 | _ properties: [String: LexiconSchema], 289 | required: [String]? = nil 290 | ) throws -> MemberBlockItemListSyntax { 291 | let required = required ?? [] 292 | let properties = properties.sorted { $0.0 < $1.0 } 293 | 294 | for (k, v) in properties { 295 | if let type = swiftTypeName(for: v) { 296 | let t = required.contains(k) ? type : "\(type)?" 297 | 298 | try VariableDeclSyntax( 299 | """ 300 | @Indirect 301 | public var \(raw: k): \(raw: t) 302 | """ 303 | ) 304 | } 305 | } 306 | } 307 | 308 | static func objectInitializerSyntax( 309 | _ properties: [String: LexiconSchema], 310 | required: [String]? = nil 311 | ) -> DeclSyntax { 312 | var signatures = [String]() 313 | var assignments = [String]() 314 | 315 | let required = required ?? [] 316 | for (k, v) in properties.sorted(by: { $0.0 < $1.0 }) { 317 | if let type = swiftTypeName(for: v) { 318 | let isRequired = required.contains(k) 319 | let t = isRequired ? type : "\(type)?" 320 | 321 | signatures.append("\(k): \(t)\(isRequired ? "" : " = nil")") 322 | assignments.append("self._\(k) = .wrapped(\(k))") 323 | } 324 | } 325 | 326 | return DeclSyntax( 327 | """ 328 | public init( 329 | \(raw: signatures.joined(separator: ",\n")) 330 | ) { 331 | \(raw: assignments.joined(separator: "\n")) 332 | } 333 | """ 334 | ) 335 | } 336 | 337 | static func requestInitializerSyntax( 338 | parameters: LexiconObjectSchema?, 339 | input: LexiconSchema? 340 | ) -> DeclSyntax { 341 | var signatures = [String]() 342 | var assignments = [String]() 343 | 344 | if parameters != nil { 345 | signatures.append("parameters: Parameters") 346 | assignments.append("self.parameters = parameters") 347 | } 348 | 349 | if input != nil { 350 | signatures.append("input: Input") 351 | assignments.append("self.input = input") 352 | } 353 | 354 | return DeclSyntax( 355 | """ 356 | public init( 357 | \(raw: signatures.joined(separator: ",\n")) 358 | ) { 359 | \(raw: assignments.joined(separator: "\n")) 360 | } 361 | """ 362 | ) 363 | } 364 | 365 | static func emptySyntax() -> MemberBlockItemListSyntax { 366 | MemberBlockItemListSyntax(stringLiteral: "") 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /Tests/LexiconGenKitTests/Generator/Generator+BuildersTests.swift: -------------------------------------------------------------------------------- 1 | import CustomDump 2 | import SwiftSyntax 3 | import SwiftSyntaxBuilder 4 | import XCTest 5 | 6 | @testable import LexiconGenKit 7 | 8 | final class GeneratorBuildersTests: XCTestCase { 9 | private typealias AbsoluteSwiftDefinition = SwiftDefinition> 10 | 11 | func testNamespaces() throws { 12 | XCTAssertNoDifference( 13 | try Generator.namespaces([ 14 | SwiftNamespaceDefinition(parent: "", name: "Foo"), 15 | ]).formatted().description, 16 | """ 17 | public enum Foo { 18 | } 19 | """ 20 | ) 21 | XCTAssertNoDifference( 22 | try Generator.namespaces([ 23 | SwiftNamespaceDefinition(parent: "", name: "Foo"), 24 | SwiftNamespaceDefinition(parent: "Foo", name: "Bar"), 25 | SwiftNamespaceDefinition(parent: "Foo.Bar", name: "Baz"), 26 | SwiftNamespaceDefinition(parent: "Foo.Bar", name: "Qux"), 27 | ]).formatted().description, 28 | """ 29 | public enum Foo { 30 | } 31 | public extension Foo { 32 | enum Bar { 33 | } 34 | } 35 | public extension Foo.Bar { 36 | enum Baz { 37 | } 38 | } 39 | public extension Foo.Bar { 40 | enum Qux { 41 | } 42 | } 43 | """ 44 | ) 45 | } 46 | 47 | func testUnknownUnion() throws { 48 | let object = LexiconObjectSchema(properties: [:], required: nil) 49 | 50 | XCTAssertNoDifference( 51 | try Generator.unknownUnion(from: [ 52 | AbsoluteSwiftDefinition(id: LexiconDefinitionID("com.example.foo#a"), parent: "Com.Example.Foo", name: "A", object: .record(object)), 53 | ]).formatted().description, 54 | """ 55 | public typealias LexiconUnknownUnion = Union1 56 | public extension LexiconUnknownUnion { 57 | var asA: Com.Example.Foo.A? { 58 | asType0 59 | } 60 | } 61 | """ 62 | ) 63 | XCTAssertNoDifference( 64 | try Generator.unknownUnion(from: [ 65 | AbsoluteSwiftDefinition(id: LexiconDefinitionID("com.example.foo#a"), parent: "Com.Example.Foo", name: "A", object: .record(object)), 66 | AbsoluteSwiftDefinition(id: LexiconDefinitionID("com.example.foo#b"), parent: "Com.Example.Foo", name: "B", object: .record(object)), 67 | AbsoluteSwiftDefinition(id: LexiconDefinitionID("com.example.Bar#b"), parent: "Com.Example.Bar", name: "B", object: .record(object)), 68 | AbsoluteSwiftDefinition(id: LexiconDefinitionID("com.example.Baz#c"), parent: "Com.Example.Baz", name: "C", object: .boolean), 69 | ]).formatted().description, 70 | """ 71 | public typealias LexiconUnknownUnion = Union3 72 | public extension LexiconUnknownUnion { 73 | var asA: Com.Example.Foo.A? { 74 | asType0 75 | } 76 | var asFooB: Com.Example.Foo.B? { 77 | asType1 78 | } 79 | var asBarB: Com.Example.Bar.B? { 80 | asType2 81 | } 82 | } 83 | """ 84 | ) 85 | } 86 | 87 | func testDefinition() throws { 88 | func makeDefinition(_ object: LexiconSchema) throws -> AbsoluteSwiftDefinition { 89 | try AbsoluteSwiftDefinition( 90 | id: LexiconDefinitionID("com.example.foo#main"), 91 | parent: "Com.Example", 92 | name: "Foo", 93 | object: object 94 | ) 95 | } 96 | 97 | // boolean 98 | XCTAssertNoDifference( 99 | try Generator.definition(makeDefinition(.boolean)).formatted().description, 100 | """ 101 | typealias Foo = Bool 102 | """ 103 | ) 104 | 105 | // integer 106 | XCTAssertNoDifference( 107 | try Generator.definition(makeDefinition(.integer)).formatted().description, 108 | """ 109 | typealias Foo = Int 110 | """ 111 | ) 112 | 113 | // string 114 | XCTAssertNoDifference( 115 | try Generator.definition(makeDefinition(.string(format: nil))).formatted().description, 116 | """ 117 | typealias Foo = String 118 | """ 119 | ) 120 | XCTAssertNoDifference( 121 | try Generator.definition(makeDefinition(.string(format: "at-uri"))).formatted().description, 122 | """ 123 | typealias Foo = ATURI 124 | """ 125 | ) 126 | XCTAssertNoDifference( 127 | try Generator.definition(makeDefinition(.string(format: "datetime"))).formatted().description, 128 | """ 129 | typealias Foo = Date 130 | """ 131 | ) 132 | XCTAssertNoDifference( 133 | try Generator.definition(makeDefinition(.string(format: "uri"))).formatted().description, 134 | """ 135 | typealias Foo = SafeURL 136 | """ 137 | ) 138 | 139 | // array 140 | XCTAssertNoDifference( 141 | try Generator.definition(makeDefinition(.array(.boolean))).formatted().description, 142 | """ 143 | typealias Foo = [Bool] 144 | """ 145 | ) 146 | XCTAssertNoDifference( 147 | try Generator.definition(makeDefinition(.array(.integer))).formatted().description, 148 | """ 149 | typealias Foo = [Int] 150 | """ 151 | ) 152 | 153 | // union 154 | XCTAssertNoDifference( 155 | try Generator.definition( 156 | makeDefinition( 157 | .union( 158 | [ 159 | LexiconAbsoluteReference(LexiconDefinitionID("com.example.foo#main")), 160 | LexiconAbsoluteReference(LexiconDefinitionID("com.example.foo#record")), 161 | ] 162 | ) 163 | ) 164 | ).formatted().description, 165 | """ 166 | typealias Foo = Union2 167 | """ 168 | ) 169 | 170 | // object 171 | XCTAssertNoDifference( 172 | try Generator.definition( 173 | makeDefinition( 174 | .object( 175 | LexiconObjectSchema( 176 | properties: [ 177 | "requiredValue": .integer, 178 | "optionalValue": .string(format: nil) 179 | ], 180 | required: ["requiredValue"] 181 | ) 182 | ) 183 | ) 184 | ).formatted().description, 185 | """ 186 | struct Foo: UnionCodable, Hashable { 187 | @Indirect 188 | public var optionalValue: String? 189 | @Indirect 190 | public var requiredValue: Int 191 | public init( 192 | optionalValue: String? = nil, 193 | requiredValue: Int 194 | ) { 195 | self._optionalValue = .wrapped(optionalValue) 196 | self._requiredValue = .wrapped(requiredValue) 197 | } 198 | public static let typeValue = #LexiconDefID("com.example.foo") 199 | } 200 | """ 201 | ) 202 | 203 | // record 204 | XCTAssertNoDifference( 205 | try Generator.definition( 206 | makeDefinition( 207 | .record( 208 | LexiconObjectSchema( 209 | properties: [ 210 | "requiredValue": .integer, 211 | "optionalValue": .string(format: nil) 212 | ], 213 | required: ["requiredValue"] 214 | ) 215 | ) 216 | ) 217 | ).formatted().description, 218 | """ 219 | struct Foo: UnionCodable, Hashable { 220 | @Indirect 221 | public var optionalValue: String? 222 | @Indirect 223 | public var requiredValue: Int 224 | public init( 225 | optionalValue: String? = nil, 226 | requiredValue: Int 227 | ) { 228 | self._optionalValue = .wrapped(optionalValue) 229 | self._requiredValue = .wrapped(requiredValue) 230 | } 231 | public static let typeValue = #LexiconDefID("com.example.foo") 232 | } 233 | """ 234 | ) 235 | 236 | // query 237 | XCTAssertNoDifference( 238 | try Generator.definition( 239 | makeDefinition( 240 | .query( 241 | LexiconMethodSchema( 242 | parameters: LexiconObjectSchema(properties: [:], required: nil), 243 | input: nil, 244 | output: .object(LexiconObjectSchema(properties: [:], required: nil)) 245 | ) 246 | ) 247 | ) 248 | ).formatted().description, 249 | """ 250 | struct Foo: XRPCRequest { 251 | public struct Parameters: XRPCRequestParametersConvertible { 252 | public init( 253 | 254 | ) { 255 | 256 | } 257 | public let queryItems: [URLQueryItem] = [] 258 | } 259 | public struct Output: Decodable, Hashable { 260 | public init( 261 | 262 | ) { 263 | 264 | } 265 | } 266 | public init( 267 | parameters: Parameters 268 | ) { 269 | self.parameters = parameters 270 | } 271 | public let type = XRPCRequestType.query 272 | public let requestIdentifier = "com.example.foo" 273 | public let parameters: Parameters 274 | } 275 | """ 276 | ) 277 | XCTAssertNoDifference( 278 | try Generator.definition( 279 | makeDefinition( 280 | .query( 281 | LexiconMethodSchema( 282 | parameters: LexiconObjectSchema( 283 | properties: [ 284 | "optionalParam": .string(format: nil), 285 | "requiredParam": .integer, 286 | ], 287 | required: ["requiredParam"] 288 | ), 289 | input: nil, 290 | output: .object( 291 | LexiconObjectSchema( 292 | properties: [ 293 | "optionalValue": .string(format: nil), 294 | "requiredValue": .integer, 295 | ], 296 | required: ["requiredValue"] 297 | ) 298 | ) 299 | ) 300 | ) 301 | ) 302 | ).formatted().description, 303 | """ 304 | struct Foo: XRPCRequest { 305 | public struct Parameters: XRPCRequestParametersConvertible { 306 | @Indirect 307 | public var optionalParam: String? 308 | @Indirect 309 | public var requiredParam: Int 310 | public init( 311 | optionalParam: String? = nil, 312 | requiredParam: Int 313 | ) { 314 | self._optionalParam = .wrapped(optionalParam) 315 | self._requiredParam = .wrapped(requiredParam) 316 | } 317 | public var queryItems: [URLQueryItem] { 318 | var parameters = [URLQueryItem] () 319 | parameters.append(contentsOf: optionalParam.toQueryItems(name: "optionalParam")) 320 | parameters.append(contentsOf: requiredParam.toQueryItems(name: "requiredParam")) 321 | 322 | return parameters 323 | } 324 | } 325 | public struct Output: Decodable, Hashable { 326 | @Indirect 327 | public var optionalValue: String? 328 | @Indirect 329 | public var requiredValue: Int 330 | public init( 331 | optionalValue: String? = nil, 332 | requiredValue: Int 333 | ) { 334 | self._optionalValue = .wrapped(optionalValue) 335 | self._requiredValue = .wrapped(requiredValue) 336 | } 337 | } 338 | public init( 339 | parameters: Parameters 340 | ) { 341 | self.parameters = parameters 342 | } 343 | public let type = XRPCRequestType.query 344 | public let requestIdentifier = "com.example.foo" 345 | public let parameters: Parameters 346 | } 347 | """ 348 | ) 349 | 350 | // procedure 351 | XCTAssertNoDifference( 352 | try Generator.definition( 353 | makeDefinition( 354 | .procedure( 355 | LexiconMethodSchema( 356 | parameters: nil, 357 | input: .object( 358 | LexiconObjectSchema( 359 | properties: [ 360 | "optionalInput": .string(format: nil), 361 | "requiredInput": .integer, 362 | ], 363 | required: ["requiredInput"] 364 | ) 365 | ), 366 | output: .object( 367 | LexiconObjectSchema( 368 | properties: [ 369 | "optionalValue": .string(format: nil), 370 | "requiredValue": .integer, 371 | ], 372 | required: ["requiredValue"] 373 | ) 374 | ) 375 | ) 376 | ) 377 | ) 378 | ).formatted().description, 379 | """ 380 | struct Foo: XRPCRequest { 381 | public struct Input: Encodable { 382 | @Indirect 383 | public var optionalInput: String? 384 | @Indirect 385 | public var requiredInput: Int 386 | public init( 387 | optionalInput: String? = nil, 388 | requiredInput: Int 389 | ) { 390 | self._optionalInput = .wrapped(optionalInput) 391 | self._requiredInput = .wrapped(requiredInput) 392 | } 393 | } 394 | public struct Output: Decodable, Hashable { 395 | @Indirect 396 | public var optionalValue: String? 397 | @Indirect 398 | public var requiredValue: Int 399 | public init( 400 | optionalValue: String? = nil, 401 | requiredValue: Int 402 | ) { 403 | self._optionalValue = .wrapped(optionalValue) 404 | self._requiredValue = .wrapped(requiredValue) 405 | } 406 | } 407 | public init( 408 | input: Input 409 | ) { 410 | self.input = input 411 | } 412 | public let type = XRPCRequestType.procedure 413 | public let requestIdentifier = "com.example.foo" 414 | public let input: Input? 415 | } 416 | """ 417 | ) 418 | 419 | // subscription 420 | XCTAssertNoDifference( 421 | try Generator.definition(makeDefinition(.subscription)).formatted().description, 422 | """ 423 | enum Foo { 424 | } 425 | """ 426 | ) 427 | } 428 | 429 | func testObjectSyntax() throws { 430 | XCTAssertNoDifference( 431 | try Generator.objectSyntax( 432 | name: "Object", 433 | inheritances: [ 434 | "Protocol1", 435 | "Protocol2" 436 | ], 437 | object: LexiconObjectSchema( 438 | properties: [ 439 | "foo": .boolean, 440 | "bar": .integer, 441 | ], 442 | required: [ 443 | "foo" 444 | ] 445 | ), 446 | additionalBody: { 447 | try VariableDeclSyntax("public let baz = 123") 448 | } 449 | ).formatted().description, 450 | """ 451 | struct Object: Protocol1, Protocol2 { 452 | @Indirect 453 | public var bar: Int? 454 | @Indirect 455 | public var foo: Bool 456 | public init( 457 | bar: Int? = nil, 458 | foo: Bool 459 | ) { 460 | self._bar = .wrapped(bar) 461 | self._foo = .wrapped(foo) 462 | } 463 | public let baz = 123 464 | } 465 | """ 466 | ) 467 | } 468 | } 469 | --------------------------------------------------------------------------------