Swift

SwiftAvroCore

SwiftAvroCore 框架实现了 Apache Avro™ 中所需的核心编码功能。模式格式支持 Avro 1.8.2 及更高版本规范。它提供了 Swift 5 中引入的用户友好的 Codable 接口,用于编码和解码 Avro 模式、二进制数据以及 JSON 格式数据。

它旨在实现以下目标

本项目 SwiftAvroCore 为所有提供 Foundation 框架的 Swift 平台提供了编码 API 的实现。Avro 规范中定义的文件 IO 和 RPC 功能将在一个单独的项目 SwiftAvroRpc 中提供,该项目依赖于 swift-nio 框架。

入门指南

SwiftAvroCore 使用 SwiftPM 作为其构建工具。如果您想在自己的项目中使用 SwiftAvroCore,只需在您的 Package.swift 文件中添加一个 dependencies 子句即可

dependencies: [ .package(url: "https://github.com/lynixliu/SwiftAvroCore") ] 然后将 SwiftAvroCore 模块添加到您的目标依赖项中。

要在 SwiftAvroCore 本身工作,或研究一些演示应用程序,您可以直接克隆存储库并使用 SwiftPM 来帮助构建它。例如,您可以运行以下命令来编译和运行示例

swift build swift test

要生成一个 Xcode 项目以便在 Xcode 中使用 SwiftAvroCore

swift package generate-xcodeproj

这会使用 SwiftPM 生成一个 Xcode 项目。您可以使用以下命令打开项目

open SwiftAvroCore.xcodeproj

使用 SwiftAvroCore

本指南假设您已安装最新版本的 Swift 二进制发行版。假设您有一个 JSON 格式的模式,如下所示

// The JSON schema
let jsonSchema = """
{"type":"record",
"fields":[
{"name": "requestId", "type": "int"},
{"name": "requestName", "type": "string"},
{"name": "parameter", "type": {"type":"array", "items": "int"}}
]}
"""

编码和解码

这是一个简单的 main.swift 文件,它使用了 SwiftAvroCore。

// main.swift
import Foundation
import SwiftAvroCore

// Define your model in Swift
struct Model: Encodable {
    let requestId: Int32 = 1
    let requestName: String = "hello"
    let parameter: [Int32] = []
}

// Make an Avro instance
let avro = Avro()
let myModel = Model(requestId: 42, requestName: "hello", parameter: [1,2])

// Decode schema from json
_ = avro.decodeSchema(schema: jsonSchema)!

// encode to avro binray
let binaryValue = try!avro.encode(myModel)

// decode from avro binary
let decodedValue: Model = try! avro.decode(from: binaryValue)

// check result
print("\(decodedValue.requestId), \(decodedValue.requestName), \(decodedValue.parameter)")

// decode from avro binary to Any Type in case of the receiving type unknown
let decodedAnyValue = try! avro.decode(from: binaryValue)

// check type
type(of: decodedAnyValue!)

为外部生成 JSON 模式

let encodedSchema = try avro.encodeSchema(schema: schema)

print(String(bytes: encodedSchema!, encoding: .utf8)!)

当解码为 Any 类型时的类型映射

原始类型

复杂类型

文件 IO

// define codec
let codec = NullCodec(codecName: AvroReservedConstants.NullCodec)

// define 2 File Object Containers
var oc1 = try? ObjectContainer(schema: """
{
    "type": "record",
    "name": "test",
    "fields" : [
        {"name": "a", "type": "long"},
        {"name": "b", "type": "string"}]
}
""", codec: codec)
var oc2 = oc1

// test model
struct model: Codable {
    var a: UInt64 = 1
    var b: String = "hello"
}

// add model to container
try oc1?.addObject(model())

// encode object
let out = try! oc1?.encodeObject()

// write to file
try out?.write(to: URL(fileURLWithPath: "/location/to/save/file"))

// decode object heade
try oc2?.decodeHeader(from: out!)

// decode objec
let start = oc2?.findMarker(from: out!)
try oc2?.decodeBlock(from: out!.subdata(in: start!..<out!.count))

// the result: oc1?.blocks[0].data

RPC

详情请参考 Tests/SwiftAvroCoreTests/AvroRequestResponseTest.swift

struct testArg {

// sample protocol
let supportProtocol: String = """
{
  "namespace": "com.acme",
  "protocol": "HelloWorld",
  "doc": "Protocol Greetings",
  "types": [
     {"name": "Greeting", "type": "record", "fields": [{"name": "message", "type": "string"}]},
     {"name": "Curse", "type": "error", "fields": [{"name": "message", "type": "string"}]}],
  "messages": {
    "hello": {
       "doc": "Say hello.",
       "request": [{"name": "greeting", "type": "Greeting" }],
       "response": "Greeting",
       "errors": ["Curse"]
    }
  }
}
"""

let arg = 
// client hash
let clientHash: [UInt8] = [UInt8]([0x0,0x1,0x2,0x3,0x4,0x5,0x6,0x7,0x8,0xA,0xB,0xC,0xD,0xE,0xF,0x10])

// server hash
let serverHash: [UInt8] = [UInt8]([0x1,0x1,0x2,0x3,0x4,0x5,0x6,0x7,0x8,0xA,0xB,0xC,0xD,0xE,0xF,0x10])

// create a conversation context
let context: Context = Context(requestMeta:[String: [UInt8]](),
                                   responseMeta:[String: [UInt8]]())
}

let arg = testArg() 

// create a server                                  
let server = try MessageResponse(context: arg.context, serverHash: arg.serverHash, serverProtocol: arg.supportProtocol)

// create a client
let client = try MessageRequest(context: arg.context, clientHash: arg.serverHash, clientProtocol: arg.supportProtocol)
        
// hand shake
let requestData = try client.encodeHandshakeRequest(request: HandshakeRequest(clientHash: arg.serverHash, clientProtocol: arg.supportProtocol, serverHash: arg.serverHash))
try client.addSession(hash: arg.serverHash, protocolString: arg.supportProtocol)
let (requestHandshake,resposeNone) = try server.resolveHandshakeRequest(requestData: requestData)
let (r, got) = try client.decodeResponse(responseData:resposeNone)

// request and response
struct requestMessage:Codable {
    var message: String = "requestData"
}

let requestMessageData = requestMessage()
let msgData = try client.writeRequest(messageName: "hello", parameters: [requestMessageData])
var expectData = Data()
expectData.append(contentsOf: [0,10]) // empty meta and length of message name
expectData.append("hello".data(using: .utf8)!) // message name
expectData.append(contentsOf: [22]) // length of message
expectData.append("requestData".data(using: .utf8)!) //message

let (requestHeader,request) = try server.readRequest(header: requestHandshake, from: msgData) as (RequestHeader, [requestMessage])

struct responseMessage:Codable {
    var message: String = "responseData"
}
let resMsg = responseMessage()
let resData = try server.writeResponse(header: requestHandshake, messageName: requestHeader.name, parameter: resMsg)
expectData = Data()
expectData.append(contentsOf: [0,0,24]) // empty meta, false flag and length of message name
expectData.append("responseData".data(using: .utf8)!)

let (responseHeader, gotResponse) = try client.readResponse(header: requestHandshake, messageName: "hello", from: resData)  as (ResponseHeader, [responseMessage])

// framing and deframing
var testData = Data([1,2,3,4,5])
testData.framing(frameLength: 4)
let deframed = testData.deFraming()

许可证

本软件根据 Apache 2.0 和 Anti-996 许可证获得许可。

详情请参考以下链接

https://github.com/lynixliu/SwiftAvroCore/blob/master/LICENSE.txt https://github.com/996icu/996.ICU/blob/master/LICENSE

贡献

请参阅 CONTRIBUTING.md

常见问题解答 (FAQ)

为什么此框架既不提供代码生成也不提供动态类型?

许多序列化系统(如 Thrift、Protocol Buffers 或 CORBA)使用接口描述语言 (IDL) 为用户生成代码。Avro 也为 C/C++ 或 Java 等静态语言提供了 IDL 来执行此操作。但是,对于频繁更改消息格式的情况,尤其是在跨团队开发环境中,代码生成不够灵活。Avro 中的数据始终与其对应的模式一起存储,这意味着无论我们是否提前知道模式,我们都可以随时读取序列化的项目。这使我们能够在不进行代码生成的情况下执行序列化和反序列化。大多数动态语言实现(如 Python、Ruby、JavaScript 库)不支持代码生成,但提供动态实例。然而,动态实例本身对于用户来说是一个黑匣子,无需检查模式定义。值没有键名,只有值类型。为动态实例设置值看起来更像是汇编语言,如果没有大量注释,则很难维护。
本项目不提供这两者中的任何一个,因为它们都不够优雅和简洁。感谢 Swift 4 中引入的 Codable 功能,SwiftAvroCore 可以为程序员提供更易于使用且类型安全的接口。您不仅可以使用 JSONDecoder 通过 Codable 功能从 Swift 结构中动态生成模式,还可以使用 encoder/decoder 通过 Avro Codable 功能从 Swift 结构中编码/解码数据。无需编写 IDL 或 JSON 模式,无需生成代码,也无需添加额外的注释或记住键名。此外,Codable 接口也是类型安全的,因此您可以轻松定位错误。就是这样,享受简洁吧 :)

为什么没有文件 IO 和 RPC?

因为文件 IO 和 RPC 功能(如 deflate)依赖于某些特定的平台和库。而编码功能几乎不依赖任何东西,除了 Swift 运行时也需要的 Foundation 框架。因此,将核心功能包装成一个独立的框架比组合框架更具可移植性和实用性。
文件 IO 和 RPC 将在另一个私有项目 SwiftAvroRpc 中提供,该项目依赖于 swift-nio 框架。该项目仍在开发中。但我即将把一些独立于操作系统和第三方软件包的模块部分转移到这个项目中。