Skip to content

Commit

Permalink
feat: fal.run url support (#7)
Browse files Browse the repository at this point in the history
* feat: fal.run url support

* fix: msgpack ios15 support

* chore: add basic tests and build updates

* fix: build job runs-on

* fix: remove ubuntu build

* fix: remove version matrix for now

* fix: remove test temporarily

* fix: ios version

* fix: temp remove sample build

* chore: update msgpack dependency

* feat: add fal image codable struct

* chore: update all urls to fal.run
  • Loading branch information
drochetti authored Jan 20, 2024
1 parent 2cfdb72 commit caf1e86
Show file tree
Hide file tree
Showing 19 changed files with 295 additions and 103 deletions.
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

0 comments on commit caf1e86

Please sign in to comment.