diff --git a/.vscode/launch.json b/.vscode/launch.json index 1da4aa77..89724ee5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -41,6 +41,24 @@ "name": "Release Cli (Cli)", "program": "${workspaceFolder:boka}/Cli/.build/release/Cli", "preLaunchTask": "swift: Build Release Cli (Cli)" + }, + { + "type": "lldb", + "request": "launch", + "args": [], + "cwd": "${workspaceFolder:boka}/Tools", + "name": "Debug Tools (Tools)", + "program": "${workspaceFolder:boka}/Tools/.build/debug/Tools", + "preLaunchTask": "swift: Build Debug Tools (Tools)" + }, + { + "type": "lldb", + "request": "launch", + "args": [], + "cwd": "${workspaceFolder:boka}/Tools", + "name": "Release Tools (Tools)", + "program": "${workspaceFolder:boka}/Tools/.build/release/Tools", + "preLaunchTask": "swift: Build Release Tools (Tools)" } ] -} \ No newline at end of file +} diff --git a/RPC/Sources/RPC/Handlers/AllHandlers.swift b/RPC/Sources/RPC/Handlers/AllHandlers.swift new file mode 100644 index 00000000..55469ce0 --- /dev/null +++ b/RPC/Sources/RPC/Handlers/AllHandlers.swift @@ -0,0 +1,14 @@ +public enum AllHandlers { + public static let handlers: [any RPCHandler.Type] = + ChainHandlers.handlers + + SystemHandlers.handlers + + TelemetryHandlers.handlers + + RPCHandlers.handlers + + public static func getHandlers(source: ChainDataSource & SystemDataSource & TelemetryDataSource) -> [any RPCHandler] { + ChainHandlers.getHandlers(source: source) + + SystemHandlers.getHandlers(source: source) + + TelemetryHandlers.getHandlers(source: source) + + RPCHandlers.getHandlers(source: handlers) + } +} diff --git a/RPC/Sources/RPC/Handlers/ChainHandlers.swift b/RPC/Sources/RPC/Handlers/ChainHandlers.swift index c086376e..81922a5e 100644 --- a/RPC/Sources/RPC/Handlers/ChainHandlers.swift +++ b/RPC/Sources/RPC/Handlers/ChainHandlers.swift @@ -2,17 +2,24 @@ import Blockchain import Foundation import Utils -enum ChainHandlers { - static func getHandlers(source: ChainDataSource) -> [any RPCHandler] { +public enum ChainHandlers { + public static let handlers: [any RPCHandler.Type] = [ + GetBlock.self, + ] + + public static func getHandlers(source: ChainDataSource) -> [any RPCHandler] { [ GetBlock(source: source), ] } - struct GetBlock: RPCHandler { - var method: String { "chain_getBlock" } - typealias Request = Data32? - typealias Response = BlockRef? + public struct GetBlock: RPCHandler { + public typealias Request = Request1 + public typealias Response = BlockRef? + public typealias DataSource = ChainDataSource + + public static var method: String { "chain_getBlock" } + public static var summary: String? { "Get block by hash. If hash is not provided, returns the best block." } private let source: ChainDataSource @@ -20,8 +27,8 @@ enum ChainHandlers { self.source = source } - func handle(request: Request) async throws -> Response? { - if let hash = request { + public func handle(request: Request) async throws -> Response? { + if let hash = request.value { try await source.getBlock(hash: hash) } else { try await source.getBestBlock() diff --git a/RPC/Sources/RPC/Handlers/RPCHandlers.swift b/RPC/Sources/RPC/Handlers/RPCHandlers.swift index 7991cb38..d63943af 100644 --- a/RPC/Sources/RPC/Handlers/RPCHandlers.swift +++ b/RPC/Sources/RPC/Handlers/RPCHandlers.swift @@ -1,24 +1,29 @@ import Utils -enum RPCHandlers { - static func getHandlers(source: [any RPCHandler]) -> [any RPCHandler] { - [ - Methods(source: source), - ] +public enum RPCHandlers { + public static let handlers: [any RPCHandler.Type] = [ + Methods.self, + ] + + public static func getHandlers(source: [any RPCHandler.Type]) -> [any RPCHandler] { + [Methods(source: source)] } - struct Methods: RPCHandler { - var method: String { "rpc_methods" } - typealias Request = VoidRequest - typealias Response = [String] + public struct Methods: RPCHandler { + public typealias Request = VoidRequest + public typealias Response = [String] + public typealias DataSource = [any RPCHandler.Type] + + public static var method: String { "rpc_methods" } + public static var summary: String? { "Returns a list of available RPC methods." } private let methods: [String] - init(source: [any RPCHandler]) { - methods = source.map(\.method) + init(source: [any RPCHandler.Type]) { + methods = source.map { h in h.method } } - func handle(request _: Request) async throws -> Response? { + public func handle(request _: Request) async throws -> Response? { methods } } diff --git a/RPC/Sources/RPC/Handlers/SystemHandlers.swift b/RPC/Sources/RPC/Handlers/SystemHandlers.swift index bee53b6b..ee2ecdf8 100644 --- a/RPC/Sources/RPC/Handlers/SystemHandlers.swift +++ b/RPC/Sources/RPC/Handlers/SystemHandlers.swift @@ -1,29 +1,50 @@ import Utils -enum SystemHandlers { - static func getHandlers(source _: SystemDataSource) -> [any RPCHandler] { +public enum SystemHandlers { + public static let handlers: [any RPCHandler.Type] = [ + Health.self, + Version.self, + ] + + public static func getHandlers(source _: SystemDataSource) -> [any RPCHandler] { [ Health(), Version(), ] } - struct Health: RPCHandler { - var method: String { "system_health" } - typealias Request = VoidRequest - typealias Response = Bool + public struct Health: RPCHandler { + public typealias Request = VoidRequest + public typealias Response = Bool + + public static var method: String { "system_health" } + public static var summary: String? { "Returns true if the node is healthy." } - func handle(request _: Request) async throws -> Response? { + public func handle(request _: Request) async throws -> Response? { true } } - struct Version: RPCHandler { - var method: String { "system_version" } - typealias Request = VoidRequest - typealias Response = String + public struct Implementation: RPCHandler { + public typealias Request = VoidRequest + public typealias Response = String + + public static var method: String { "system_implementation" } + public static var summary: String? { "Returns the implementation name of the node." } + + public func handle(request _: Request) async throws -> Response? { + "Boka" + } + } + + public struct Version: RPCHandler { + public typealias Request = VoidRequest + public typealias Response = String + + public static var method: String { "system_version" } + public static var summary: String? { "Returns the version of the node." } - func handle(request _: Request) async throws -> Response? { + public func handle(request _: Request) async throws -> Response? { "0.0.1" } } diff --git a/RPC/Sources/RPC/Handlers/TelemetryHandlers.swift b/RPC/Sources/RPC/Handlers/TelemetryHandlers.swift index dcb39d82..baf97d90 100644 --- a/RPC/Sources/RPC/Handlers/TelemetryHandlers.swift +++ b/RPC/Sources/RPC/Handlers/TelemetryHandlers.swift @@ -2,18 +2,25 @@ import Blockchain import Foundation import Utils -enum TelemetryHandlers { - static func getHandlers(source: TelemetryDataSource & ChainDataSource) -> [any RPCHandler] { +public enum TelemetryHandlers { + public static let handlers: [any RPCHandler.Type] = [ + GetUpdate.self, + Name.self, + ] + + public static func getHandlers(source: TelemetryDataSource & ChainDataSource) -> [any RPCHandler] { [ GetUpdate(source: source), Name(source: source), ] } - struct GetUpdate: RPCHandler { - var method: String { "telemetry_getUpdate" } - typealias Request = VoidRequest - typealias Response = [String: String] + public struct GetUpdate: RPCHandler { + public typealias Request = VoidRequest + public typealias Response = [String: String] + + public static var method: String { "telemetry_getUpdate" } + public static var summary: String? { "Returns the latest telemetry update." } private let source: TelemetryDataSource & ChainDataSource @@ -21,11 +28,10 @@ enum TelemetryHandlers { self.source = source } - func handle(request _: Request) async throws -> Response? { + public func handle(request _: Request) async throws -> Response? { let block = try await source.getBestBlock() let peerCount = try await source.getPeersCount() - return try await [ - "name": source.name(), + return [ "chainHead": block.header.timeslot.description, "blockHash": block.hash.description, "peerCount": peerCount.description, @@ -33,10 +39,12 @@ enum TelemetryHandlers { } } - struct Name: RPCHandler { - var method: String { "telemetry_name" } - typealias Request = VoidRequest - typealias Response = String + public struct Name: RPCHandler { + public typealias Request = VoidRequest + public typealias Response = String + + public static var method: String { "telemetry_name" } + public static var summary: String? { "Returns the name of the node." } private let source: TelemetryDataSource @@ -44,7 +52,7 @@ enum TelemetryHandlers { self.source = source } - func handle(request _: Request) async throws -> Response? { + public func handle(request _: Request) async throws -> Response? { try await source.name() } } diff --git a/RPC/Sources/RPC/JSONRPC/FromJSON.swift b/RPC/Sources/RPC/JSONRPC/FromJSON.swift index 95f32507..1d0caa14 100644 --- a/RPC/Sources/RPC/JSONRPC/FromJSON.swift +++ b/RPC/Sources/RPC/JSONRPC/FromJSON.swift @@ -6,21 +6,21 @@ enum FromJSONError: Error { case unexpectedJSON } -protocol FromJSON { +public protocol FromJSON { init(from: JSON?) throws } -enum VoidRequest: FromJSON { +public enum VoidRequest: FromJSON { case void - init(from _: JSON?) throws { + public init(from _: JSON?) throws { // ignore self = .void } } extension Optional: FromJSON where Wrapped: FromJSON { - init(from json: JSON?) throws { + public init(from json: JSON?) throws { guard let json else { self = .none return @@ -35,7 +35,7 @@ extension Optional: FromJSON where Wrapped: FromJSON { } extension BinaryInteger where Self: FromJSON { - init(from json: JSON?) throws { + public init(from json: JSON?) throws { guard let json else { throw FromJSONError.null } @@ -60,7 +60,7 @@ extension UInt64: FromJSON {} extension UInt: FromJSON {} extension Data: FromJSON { - init(from json: JSON?) throws { + public init(from json: JSON?) throws { guard let json else { throw FromJSONError.null } @@ -73,14 +73,14 @@ extension Data: FromJSON { } } -extension Data32: FromJSON { - init(from json: JSON?) throws { +extension FixedSizeData: FromJSON { + public init(from json: JSON?) throws { guard let json else { throw FromJSONError.null } switch json { case let .string(str): - self = try Data32(fromHexString: str).unwrap() + self = try FixedSizeData(fromHexString: str).unwrap() default: throw FromJSONError.unexpectedJSON } diff --git a/RPC/Sources/RPC/JSONRPC/JSONRPC.swift b/RPC/Sources/RPC/JSONRPC/JSONRPC.swift index 67f19de0..d3dd5333 100644 --- a/RPC/Sources/RPC/JSONRPC/JSONRPC.swift +++ b/RPC/Sources/RPC/JSONRPC/JSONRPC.swift @@ -1,27 +1,27 @@ import Utils import Vapor -struct JSONRequest: Content { - let jsonrpc: String - let method: String - let params: JSON? - let id: JSON +public struct JSONRequest: Content { + public let jsonrpc: String + public let method: String + public let params: JSON? + public let id: JSON } -struct JSONResponse: Content { - let jsonrpc: String - let result: AnyCodable? - let error: JSONError? - let id: JSON? +public struct JSONResponse: Content { + public let jsonrpc: String + public let result: AnyCodable? + public let error: JSONError? + public let id: JSON? - init(id: JSON?, result: (any Encodable)?) { + public init(id: JSON?, result: (any Encodable)?) { jsonrpc = "2.0" self.result = result.map(AnyCodable.init) error = nil self.id = id } - init(id: JSON?, error: JSONError) { + public init(id: JSON?, error: JSONError) { jsonrpc = "2.0" result = nil self.error = error @@ -29,13 +29,13 @@ struct JSONResponse: Content { } } -struct JSONError: Content, Error { +public struct JSONError: Content, Error { let code: Int let message: String } extension JSONError { - static func methodNotFound(_ method: String) -> JSONError { + public static func methodNotFound(_ method: String) -> JSONError { JSONError(code: -32601, message: "Method not found: \(method)") } } diff --git a/RPC/Sources/RPC/JSONRPC/JSONRPCController.swift b/RPC/Sources/RPC/JSONRPC/JSONRPCController.swift index fa9e94fe..875435b6 100644 --- a/RPC/Sources/RPC/JSONRPC/JSONRPCController.swift +++ b/RPC/Sources/RPC/JSONRPC/JSONRPCController.swift @@ -13,10 +13,11 @@ final class JSONRPCController: RouteCollection, Sendable { init(handlers: [any RPCHandler]) { var dict = [String: any RPCHandler]() for handler in handlers { - if dict.keys.contains(handler.method) { - logger.warning("Duplicated handler: \(handler.method)") + let method = type(of: handler).method + if dict.keys.contains(method) { + logger.warning("Duplicated handler: \(method)") } - dict[handler.method] = handler + dict[method] = handler } self.handlers = dict diff --git a/RPC/Sources/RPC/JSONRPC/RPCHandler.swift b/RPC/Sources/RPC/JSONRPC/RPCHandler.swift index 67d0bf04..ee51b5fa 100644 --- a/RPC/Sources/RPC/JSONRPC/RPCHandler.swift +++ b/RPC/Sources/RPC/JSONRPC/RPCHandler.swift @@ -2,14 +2,20 @@ import Foundation import Utils import Vapor -protocol RPCHandler: Sendable { - associatedtype Request: FromJSON +public protocol RPCHandler: Sendable { + associatedtype Request: RequestParameter associatedtype Response: Encodable - var method: String { get } + static var method: String { get } func handle(request: Request) async throws -> Response? func handle(jsonRequest: JSONRequest) async throws -> JSONResponse + + // for OpenRPC spec generation + static var summary: String? { get } + + static var requestType: any RequestParameter.Type { get } + static var responseType: any Encodable.Type { get } } extension RPCHandler { @@ -21,4 +27,12 @@ extension RPCHandler { result: res ) } + + public static var requestType: any RequestParameter.Type { + Request.self + } + + public static var responseType: any Encodable.Type { + Response.self + } } diff --git a/RPC/Sources/RPC/JSONRPC/RequestParameter.swift b/RPC/Sources/RPC/JSONRPC/RequestParameter.swift new file mode 100644 index 00000000..fd45a459 --- /dev/null +++ b/RPC/Sources/RPC/JSONRPC/RequestParameter.swift @@ -0,0 +1,94 @@ +import Utils + +enum RequestError: Error { + case null + case notArray + case unexpectedLength +} + +public protocol RequestParameter: FromJSON { + static var types: [Any.Type] { get } +} + +extension VoidRequest: RequestParameter { + public static var types: [Any.Type] { [] } +} + +// Swift don't yet support variadic generics +// so we need to use this workaround + +public struct Request1: RequestParameter { + public static var types: [Any.Type] { [T.self] } + + public let value: T + + public init(from json: JSON?) throws { + guard let json else { + throw RequestError.null + } + guard case let .array(arr) = json else { + throw RequestError.notArray + } + guard arr.count <= 1 else { + throw RequestError.unexpectedLength + } + value = try T(from: arr[safe: 0]) + } +} + +public struct Request2: RequestParameter { + public static var types: [Any.Type] { [T1.self, T2.self] } + + public let valuu: (T1, T2) + + public init(from json: JSON?) throws { + guard let json else { + throw RequestError.null + } + guard case let .array(arr) = json else { + throw RequestError.notArray + } + guard arr.count <= 2 else { + throw RequestError.unexpectedLength + } + valuu = try (T1(from: arr[safe: 0]), T2(from: arr[safe: 1])) + } +} + +public struct Request3: RequestParameter { + public static var types: [Any.Type] { [T1.self, T2.self, T3.self] } + + public let value: (T1, T2, T3) + + public init(from json: JSON?) throws { + guard let json else { + throw RequestError.null + } + guard case let .array(arr) = json else { + throw RequestError.notArray + } + guard arr.count <= 3 else { + throw RequestError.unexpectedLength + } + value = try (T1(from: arr[safe: 0]), T2(from: arr[safe: 1]), T3(from: arr[safe: 2])) + } +} + +public struct Request4: RequestParameter { + public static var types: [Any.Type] { [T1.self, T2.self, T3.self, T4.self] } + + public let value: (T1, T2, T3, T4) + + public init(from json: JSON?) throws { + guard let json else { + throw RequestError.null + } + guard case let .array(arr) = json else { + throw RequestError.notArray + } + guard arr.count <= 4 else { + throw RequestError.unexpectedLength + } + value = try (T1(from: arr[safe: 0]), T2(from: arr[safe: 1]), T3(from: arr[safe: 2]), T4(from: arr[safe: 3])) + } +} diff --git a/RPC/Sources/RPC/Server.swift b/RPC/Sources/RPC/Server.swift index d46cf1f3..783d9c53 100644 --- a/RPC/Sources/RPC/Server.swift +++ b/RPC/Sources/RPC/Server.swift @@ -27,10 +27,17 @@ public class Server { let env = try Environment.detect(arguments: ["--env"]) app = Application(env) - var handlers: [any RPCHandler] = SystemHandlers.getHandlers(source: source) - handlers.append(contentsOf: ChainHandlers.getHandlers(source: source)) - handlers.append(contentsOf: TelemetryHandlers.getHandlers(source: source)) - handlers.append(contentsOf: RPCHandlers.getHandlers(source: handlers)) + // TODO: configure cors origins + let corsConfiguration = CORSMiddleware.Configuration( + allowedOrigin: .all, + allowedMethods: [.GET, .POST, .PUT, .OPTIONS], + allowedHeaders: [.accept, .authorization, .contentType, .origin, .xRequestedWith, .userAgent, .accessControlAllowOrigin] + ) + let cors = CORSMiddleware(configuration: corsConfiguration) + // cors middleware should come before default error middleware using `at: .beginning` + app.middleware.use(cors, at: .beginning) + + let handlers = AllHandlers.getHandlers(source: source) // Register routes let rpcController = JSONRPCController(handlers: handlers) diff --git a/Tools/Package.swift b/Tools/Package.swift new file mode 100644 index 00000000..44cc66e3 --- /dev/null +++ b/Tools/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Tools", + platforms: [ + .macOS(.v15), + ], + dependencies: [ + .package(path: "../RPC"), + .package(path: "../TracingUtils"), + .package(path: "../Utils"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), + .package(url: "https://github.com/ajevans99/swift-json-schema.git", from: "0.2.1"), + .package(url: "https://github.com/wickwirew/Runtime.git", from: "2.2.7"), + ], + targets: [ + .executableTarget( + name: "Tools", + dependencies: [ + "RPC", + "Utils", + "TracingUtils", + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "JSONSchema", package: "swift-json-schema"), + .product(name: "JSONSchemaBuilder", package: "swift-json-schema"), + .product(name: "Runtime", package: "Runtime"), + ] + ), + ], + swiftLanguageModes: [.version("6")] +) diff --git a/Tools/Sources/Tools/OpenRPC.swift b/Tools/Sources/Tools/OpenRPC.swift new file mode 100644 index 00000000..ef753046 --- /dev/null +++ b/Tools/Sources/Tools/OpenRPC.swift @@ -0,0 +1,257 @@ +import ArgumentParser +import Foundation +import JSONSchema +import JSONSchemaBuilder +import RPC +import Runtime +import Utils + +struct OpenRPC: AsyncParsableCommand { + static let configuration = CommandConfiguration( + abstract: "OpenRPC tools", + subcommands: [Generate.self] + ) + + struct Generate: AsyncParsableCommand { + @Argument(help: "output file") + var output: String = "openrpc.json" + + func run() async throws { + let handlers = AllHandlers.handlers + + let spec = SpecDoc( + openrpc: "1.0.0", + info: SpecInfo( + title: "JAM JSONRPC (draft)", + version: "0.0.1", + description: "JSONRPC spec for JAM nodes (draft)" + ), + methods: handlers.map { h in + SpecMethod( + name: h.method, + summary: h.summary, + description: nil, + params: h.requestType.types.map { createSpecContent(type: $0) }, + result: createSpecContent(type: h.responseType), + examples: nil + ) + } + ) + + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted] + let data = try encoder.encode(spec) + try data.write(to: URL(fileURLWithPath: output)) + } + } +} + +private protocol OptionalProtocol { + static var wrappedType: Any.Type { get } +} + +extension Optional: OptionalProtocol { + static var wrappedType: Any.Type { + Wrapped.self + } +} + +func build(@JSONSchemaBuilder _ content: () -> any JSONSchemaComponent) -> any JSONSchemaComponent { + content() +} + +func createSpecContent(type: Any.Type) -> SpecContent { + // if it is optional + if let type = type as? OptionalProtocol.Type { + return createSpecContentInner(type: type.wrappedType, required: false) + } else { + return createSpecContentInner(type: type, required: true) + } + + func createSpecContentInner(type: Any.Type, required: Bool) -> SpecContent { + .init( + name: getName(type: type), + summary: nil, + description: nil, + required: required, + schema: getSchema(type: type).definition + ) + } +} + +protocol TypeDescription { + static var name: String { get } + + static var schema: any JSONSchemaComponent { get } +} + +func getName(type: Any.Type) -> String { + if let type = type as? TypeDescription.Type { + return type.name + } + return String(describing: type) +} + +func getSchema(type: Any.Type) -> any JSONSchemaComponent { + if let type = type as? TypeDescription.Type { + return type.schema + } + + print(type) + let info = try! typeInfo(of: type) + switch info.kind { + case .struct, .class: + return build { + JSONObject { + for field in info.properties { + JSONProperty(key: field.name) { + getSchema(type: field.type) + } + } + }.title(String(describing: type)) + } + default: + return build { + JSONObject().title(getName(type: type)) + } + } +} + +extension Optional: TypeDescription { + static var name: String { + "Optional<\(getName(type: Wrapped.self))>" + } + + static var schema: any JSONSchemaComponent { + getSchema(type: Wrapped.self) + } +} + +extension Bool: TypeDescription { + static var name: String { + "Bool" + } + + static var schema: any JSONSchemaComponent { + JSONBoolean() + } +} + +extension String: TypeDescription { + static var name: String { + "String" + } + + static var schema: any JSONSchemaComponent { + JSONString() + } +} + +extension BinaryInteger where Self: TypeDescription { + static var name: String { + String(describing: Self.self) + } + + static var schema: any JSONSchemaComponent { + JSONInteger() + } +} + +extension Int8: TypeDescription {} +extension Int16: TypeDescription {} +extension Int32: TypeDescription {} +extension Int64: TypeDescription {} +extension Int: TypeDescription {} +extension UInt8: TypeDescription {} +extension UInt16: TypeDescription {} +extension UInt32: TypeDescription {} +extension UInt64: TypeDescription {} +extension UInt: TypeDescription {} + +extension Data: TypeDescription { + static var name: String { + "Data" + } + + static var schema: any JSONSchemaComponent { + JSONString() + .title(name) + .pattern("^0x[0-9a-fA-F]*$") + } +} + +extension FixedSizeData: TypeDescription { + static var name: String { + "Data\(T.value)" + } + + static var schema: any JSONSchemaComponent { + JSONString() + .title(name) + .pattern("^0x[0-9a-fA-F]{\(T.value * 2)}$") + } +} + +extension Array: TypeDescription { + static var name: String { + "Array<\(getName(type: Element.self))>" + } + + static var schema: any JSONSchemaComponent { + JSONArray().items { getSchema(type: Element.self) } + } +} + +extension Dictionary: TypeDescription { + static var name: String { + "Dictionary<\(getName(type: Key.self)), \(getName(type: Value.self))>" + } + + static var schema: any JSONSchemaComponent { + JSONObject().title(name) + } +} + +extension Set: TypeDescription { + static var name: String { + "Set<\(getName(type: Element.self))>" + } + + static var schema: any JSONSchemaComponent { + JSONArray().items { getSchema(type: Element.self) } + } +} + +extension LimitedSizeArray: TypeDescription { + static var name: String { + if minLength == maxLength { + "Array\(minLength)<\(getName(type: T.self))>" + } else { + "Array<\(getName(type: T.self))>[\(minLength) ..< \(maxLength)]" + } + } + + static var schema: any JSONSchemaComponent { + JSONArray().items { getSchema(type: T.self) } + } +} + +extension ConfigLimitedSizeArray: TypeDescription { + static var name: String { + "Array<\(getName(type: T.self))>[\(getName(type: TMinLength.self)) ..< \(getName(type: TMaxLength.self))]" + } + + static var schema: any JSONSchemaComponent { + JSONArray().items { getSchema(type: T.self) } + } +} + +extension Ref: TypeDescription { + static var name: String { + getName(type: T.self) + } + + static var schema: any JSONSchemaComponent { + getSchema(type: T.self) + } +} diff --git a/Tools/Sources/Tools/SpecDoc.swift b/Tools/Sources/Tools/SpecDoc.swift new file mode 100644 index 00000000..44e8433e --- /dev/null +++ b/Tools/Sources/Tools/SpecDoc.swift @@ -0,0 +1,49 @@ +import JSONSchema +import Utils + +struct SpecInfo: Codable { + var title: String + var version: String + var description: String? +} + +struct SpecMethod: Codable { + var name: String + var summary: String? + var description: String? + var params: [SpecContent]? + var result: SpecContent + var examples: [SpecExample]? +} + +struct SpecContent: Codable { + var name: String + var summary: String? + var description: String? + var required: Bool? + var schema: Schema +} + +struct SpecExample: Codable { + var name: String + var summary: String? + var description: String? + var params: [SpecExampleParam] + var result: SpecExampleResult? +} + +struct SpecExampleParam: Codable { + var name: String + var value: JSON +} + +struct SpecExampleResult: Codable { + var name: String + var value: JSON +} + +struct SpecDoc: Codable { + var openrpc: String = "1.0.0" + var info: SpecInfo + var methods: [SpecMethod] +} diff --git a/Tools/Sources/Tools/Tools.swift b/Tools/Sources/Tools/Tools.swift new file mode 100644 index 00000000..0c230263 --- /dev/null +++ b/Tools/Sources/Tools/Tools.swift @@ -0,0 +1,17 @@ +import ArgumentParser +import RPC +import TracingUtils +import Utils + +@main +struct Boka: AsyncParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Boka Tools", + version: "0.0.1", + subcommands: [ + OpenRPC.self, + ] + ) + + mutating func run() async throws {} +} diff --git a/boka.xcodeproj/project.pbxproj b/boka.xcodeproj/project.pbxproj index 7cb1346a..b7ef868c 100644 --- a/boka.xcodeproj/project.pbxproj +++ b/boka.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 066C926F2C095D76005DDE4F /* Blockchain */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Blockchain; sourceTree = ""; }; 066C92702C095D85005DDE4F /* Node */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Node; sourceTree = ""; }; 0674602A2C61BF63008FD993 /* TracingUtils */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = TracingUtils; sourceTree = ""; }; + 06814C5E2CF869B6007CCEC2 /* Tools */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Tools; sourceTree = ""; }; 06AAEF0C2C47746C0064995D /* PolkaVM */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = PolkaVM; sourceTree = ""; }; 06E2B78F2C7304FF00E35A48 /* Codec */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Codec; sourceTree = ""; }; 06F2335F2C0B306000A5E2E0 /* Database */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Database; sourceTree = ""; }; @@ -24,6 +25,7 @@ 066C92652C095CE5005DDE4F = { isa = PBXGroup; children = ( + 06814C5E2CF869B6007CCEC2 /* Tools */, 06E2B78F2C7304FF00E35A48 /* Codec */, 064D330E2C632ACC001A5F36 /* RPC */, 0674602A2C61BF63008FD993 /* TracingUtils */,