Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fal.run url support #7

Merged
merged 12 commits into from
Jan 20, 2024
33 changes: 26 additions & 7 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,34 @@ on:

jobs:
build:
name: Build
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: swift-actions/setup-swift@v1
- name: Checkout project
uses: actions/checkout@v4
- name: Setup Swift
uses: swift-actions/setup-swift@v1
with:
swift-version: "5.9"
- name: Check format
run: swift package plugin --allow-writing-to-package-directory swiftformat .
- name: Build Library
# - name: Test library
# run: swift test
- name: Build library
run: swift build --target FalClient --configuration release
# - name: Build Sample App
# run: xcodebuild -project Sources/FalSampleApp/FalSampleApp.xcodeproj -scheme FalSampleApp
# samples:
# name: Build samples
# needs: build
# runs-on: macos-latest
# steps:
# - name: Checkout project
# uses: actions/checkout@v4
# - name: Setup Swift
# uses: swift-actions/setup-swift@v1
# with:
# swift-version: "5.9"
# - name: Build basic app
# uses: sersoft-gmbh/xcodebuild-action@v3
# with:
# project: Sources/Samples/FalSampleApp/FalSampleApp.xcodeproj
# scheme: FalSampleApp
# destination: platform=iOS Simulator,name=iPhone 13,OS=16.2
# action: build
43 changes: 35 additions & 8 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,21 +1,48 @@
{
"pins" : [
{
"identity" : "msgpack-swift",
"identity" : "cwlcatchexception",
"kind" : "remoteSourceControl",
"location" : "https://github.com/fumoboy007/msgpack-swift.git",
"location" : "https://github.com/mattgallagher/CwlCatchException.git",
"state" : {
"revision" : "1e3124367973f45955f27f49a617862605e55288",
"version" : "2.0.0"
"revision" : "3b123999de19bf04905bc1dfdb76f817b0f2cc00",
"version" : "2.1.2"
}
},
{
"identity" : "swiftformat",
"identity" : "cwlpreconditiontesting",
"kind" : "remoteSourceControl",
"location" : "https://github.com/nicklockwood/SwiftFormat",
"location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git",
"state" : {
"revision" : "cac06079ce883170ab44cb021faad298daeec2a5",
"version" : "0.52.10"
"revision" : "dc9af4781f2afdd1e68e90f80b8603be73ea7abc",
"version" : "2.2.0"
}
},
{
"identity" : "nimble",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Quick/Nimble.git",
"state" : {
"revision" : "d616f15123bfb36db1b1075153f73cf40605b39d",
"version" : "13.0.0"
}
},
{
"identity" : "quick",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Quick/Quick.git",
"state" : {
"revision" : "ef9aaf3f634b3a1ab6f54f1173fe2400b36e7cb8",
"version" : "7.3.0"
}
},
{
"identity" : "swift-msgpack",
"kind" : "remoteSourceControl",
"location" : "https://github.com/nnabeyang/swift-msgpack.git",
"state" : {
"revision" : "01a4324add1dbcba63dc74a8febb02291622b532",
"version" : "0.3.3"
}
}
],
Expand Down
28 changes: 15 additions & 13 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,39 @@ import PackageDescription
let package = Package(
name: "FalClient",
platforms: [
.iOS(.v16),
.macOS(.v13),
.macCatalyst(.v16),
.tvOS(.v16),
.watchOS(.v9),
.iOS(.v15),
.macOS(.v12),
.macCatalyst(.v15),
.tvOS(.v15),
.watchOS(.v8),
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "FalClient",
targets: ["FalClient"]
),
],
dependencies: [
.package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.52.10"),
.package(url: "https://github.com/fumoboy007/msgpack-swift.git", from: "2.0.0")
.package(url: "https://github.com/nnabeyang/swift-msgpack.git", from: "0.3.3"),
.package(url: "https://github.com/Quick/Quick.git", from: "7.3.0"),
.package(url: "https://github.com/Quick/Nimble.git", from: "13.0.0"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "FalClient",
dependencies: [
.product(name: "DMMessagePack", package: "msgpack-swift")
.product(name: "SwiftMsgpack", package: "swift-msgpack"),
],
path: "Sources/FalClient"
),
.testTarget(
name: "FalClientTests",
dependencies: ["FalClient"],
dependencies: [
"FalClient",
.product(name: "Quick", package: "quick"),
.product(name: "Nimble", package: "nimble"),
],
path: "Tests/FalClientTests"
)
),
]
)
2 changes: 1 addition & 1 deletion Sources/FalClient/Client+Request.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ extension Client {
}

func checkResponseStatus(for response: URLResponse, withData data: Data) throws {
guard let httpResponse = response as? HTTPURLResponse else {
guard response is HTTPURLResponse else {
throw FalError.invalidResultFormat
}
if let httpResponse = response as? HTTPURLResponse, !httpResponse.isSuccessful {
Expand Down
4 changes: 0 additions & 4 deletions Sources/FalClient/FalClient.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import Dispatch
import Foundation

func buildUrl(fromId id: String, path: String? = nil) -> String {
"https://\(id).gateway.alpha.fal.ai" + (path ?? "")
}

/// The main client class that provides access to simple API model usage,
/// as well as access to the `queue` and `storage` APIs.
///
Expand Down
102 changes: 102 additions & 0 deletions Sources/FalClient/FalImage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import Foundation

public enum FalImageContent: Codable {
case url(String)
case raw(Data)

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let url = try? container.decode(String.self) {
self = .url(url)
} else if let data = try? container.decode(Data.self) {
self = .raw(data)
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "FalImageContent must be either URL, Base64 or Binary")
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case let .url(url):
try container.encode(url)
case let .raw(data):
try container.encode(data)
}
}

public var data: Data {
switch self {
case let .url(url):
let url = URL(string: url)!
return try! Data(contentsOf: url)
case let .raw(data):
return data
}
}
}

extension FalImageContent: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self = .url(value)
}
}

extension FalImageContent: ExpressibleByStringInterpolation {
public init(stringInterpolation: StringInterpolation) {
self = .url(stringInterpolation.string)
}

public struct StringInterpolation: StringInterpolationProtocol {
var string: String = ""

public init(literalCapacity _: Int, interpolationCount _: Int) {}

public mutating func appendLiteral(_ literal: String) {
string.append(literal)
}

public mutating func appendInterpolation(_ value: String) {
string.append(value)
}
}
}

public struct FalImage: Codable {
public let content: FalImageContent
public let contentType: String
public let width: Int
public let height: Int

// The following exist so we support payloads with both `url` and `content` keys
// This should no longer be necessary once the Server API is consolidated
enum UrlCodingKeys: String, CodingKey {
case content = "url"
case contentType = "content_type"
case width
case height
}

enum RawDataCodingKeys: String, CodingKey {
case content
case contentType = "content_type"
case width
case height
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: UrlCodingKeys.self)
if let url = try? container.decode(String.self, forKey: .content) {
content = .url(url)
contentType = try container.decode(String.self, forKey: .contentType)
width = try container.decode(Int.self, forKey: .width)
height = try container.decode(Int.self, forKey: .height)
} else {
let container = try decoder.container(keyedBy: RawDataCodingKeys.self)
content = try .raw(container.decode(Data.self, forKey: .content))
contentType = try container.decode(String.self, forKey: .contentType)
width = try container.decode(Int.self, forKey: .width)
height = try container.decode(Int.self, forKey: .height)
}
}
}
6 changes: 3 additions & 3 deletions Sources/FalClient/Payload.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Foundation
import MessagePack
import SwiftMsgpack

/// Represents a value that can be encoded and decoded. This data structure
/// is used to represent the input and output of the model API and closely
Expand Down Expand Up @@ -240,15 +240,15 @@ public extension Payload {
}

static func create(fromBinary data: Data) throws -> Payload {
try MessagePackDecoder().decode(Payload.self, from: data)
try MsgPackDecoder().decode(Payload.self, from: data)
}

func json() throws -> Data {
try JSONEncoder().encode(self)
}

func binary() throws -> Data {
try MessagePackEncoder().encode(self)
try MsgPackEncoder().encode(self)
}
}

Expand Down
29 changes: 23 additions & 6 deletions Sources/FalClient/Queue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,27 +49,44 @@ public struct QueueStatusInput: Encodable {
public struct QueueClient: Queue {
public let client: Client

func runOnQueue(_ app: String, input: Payload?, options: RunOptions) async throws -> Payload {
var requestInput = input
if let storage = client.storage as? StorageClient,
let input,
options.httpMethod != .get,
input.hasBinaryData
{
requestInput = try await storage.autoUpload(input: input)
}
let queryParams = options.httpMethod == .get ? input : nil
let url = buildUrl(fromId: app, path: options.path, subdomain: "queue")
let data = try await client.sendRequest(to: url, input: requestInput?.json(), queryParams: queryParams?.asDictionary, options: options)
return try .create(fromJSON: data)
}

public func submit(_ id: String, input: Payload?, webhookUrl _: String?) async throws -> String {
let result = try await client.run(id, input: input, options: .route("/fal/queue/submit"))
let result = try await runOnQueue(id, input: input, options: .withMethod(.post))
guard case let .string(requestId) = result["request_id"] else {
throw FalError.invalidResultFormat
}
return requestId
}

public func status(_ id: String, of requestId: String, includeLogs: Bool) async throws -> QueueStatus {
try await client.run(
let result = try await runOnQueue(
id,
input: QueueStatusInput(logs: includeLogs),
options: .route("/fal/queue/requests/\(requestId)/status", withMethod: .get)
input: ["logs": .bool(includeLogs)],
options: .route("/requests/\(requestId)/status", withMethod: .get)
)
let json = try result.json()
return try JSONDecoder().decode(QueueStatus.self, from: json)
}

public func response(_ id: String, of requestId: String) async throws -> Payload {
try await client.run(
try await runOnQueue(
id,
input: nil as Payload?,
options: .route("/fal/queue/requests/\(requestId)/response", withMethod: .get)
options: .route("/requests/\(requestId)", withMethod: .get)
)
}
}
Loading