# Platform Bindings
cooklang-find provides native bindings for iOS (Swift) and Android (Kotlin) through [UniFFI](https://mozilla.github.io/uniffi-rs/).
## Building Bindings
### Prerequisites
- Rust toolchain (install via [rustup](https://rustup.rs/))
- For iOS: macOS with Xcode
- For Android: Android NDK
### Quick Start
Generate bindings only (no cross-compilation):
```bash
# Swift bindings
./scripts/build-swift.sh --generate-only
# Kotlin bindings
./scripts/build-kotlin.sh --generate-only
```
Build for all platforms:
```bash
# Swift (requires macOS)
./scripts/build-swift.sh --all
# Kotlin/Android (requires Android NDK)
ANDROID_NDK_HOME=/path/to/ndk ./scripts/build-kotlin.sh --all
```
## Swift / iOS
### Installation
#### Swift Package Manager
Add to your `Package.swift`:
```swift
dependencies: [
.package(url: "https://github.com/cooklang/cooklang-find", from: "0.5.1")
]
```
Or add via Xcode: File → Add Package Dependencies → Enter the repository URL.
#### Manual Installation
1. Download the latest release from GitHub
2. Drag `CooklangFind.xcframework` into your Xcode project
3. Add the Swift source files to your target
### Usage
```swift
import CooklangFind
// Load a recipe by name
do {
let recipe = try getRecipe(baseDirs: ["./recipes"], name: "pancakes")
print("Recipe: \(recipe.name() ?? "Unknown")")
print("Servings: \(recipe.metadata().servings ?? 0)")
// Get recipe content
let content = try recipe.content()
print(content)
// Access step images
let stepImages = recipe.stepImages()
for image in stepImages.images {
print("Section \(image.section), Step \(image.step): \(image.imagePath)")
}
} catch let error as CooklangError {
print("Error: \(error)")
}
// Search for recipes
let results = try search(baseDir: "./recipes", query: "chocolate")
for recipe in results {
print("Found: \(recipe.name() ?? "Unknown")")
}
// Build a recipe tree
let tree = try buildTree(baseDir: "./recipes")
for node in tree.allNodes() {
print("\(node.name) - hasRecipe: \(node.hasRecipe)")
}
// Create recipe from content
let content = """
---
title: Quick Omelette
servings: 2
---
Crack @eggs{3} into a bowl and whisk.
"""
let recipe = try recipeFromContent(content: content, name: nil)
print(recipe.name()) // "Quick Omelette"
```
### Error Handling
All functions that can fail throw `CooklangError`:
```swift
do {
let recipe = try getRecipe(baseDirs: ["./recipes"], name: "nonexistent")
} catch let error as CooklangError.NotFound {
print("Recipe not found: \(error.message)")
} catch let error as CooklangError.IoError {
print("IO error: \(error.message)")
} catch {
print("Other error: \(error)")
}
```
## Kotlin / Android
### Installation
#### Gradle (GitHub Packages)
Add to your `settings.gradle.kts`:
```kotlin
dependencyResolutionManagement {
repositories {
maven {
url = uri("https://maven.pkg.github.com/cooklang/cooklang-find")
credentials {
username = System.getenv("GITHUB_ACTOR") ?: project.findProperty("gpr.user") as String?
password = System.getenv("GITHUB_TOKEN") ?: project.findProperty("gpr.token") as String?
}
}
}
}
```
Add to your `build.gradle.kts`:
```kotlin
dependencies {
implementation("org.cooklang:cooklang-find:0.5.1")
}
```
#### Manual Installation
1. Download the latest `cooklang-find-android.zip` from GitHub releases
2. Extract and copy the `cooklang-find-android` module to your project
3. Add to your `settings.gradle.kts`:
```kotlin
include(":cooklang-find-android")
```
4. Add to your app's `build.gradle.kts`:
```kotlin
dependencies {
implementation(project(":cooklang-find-android"))
}
```
### Usage
```kotlin
import org.cooklang.find.*
// Load a recipe by name
try {
val recipe = getRecipe(listOf("./recipes"), "pancakes")
println("Recipe: ${recipe.name()}")
println("Servings: ${recipe.metadata().servings}")
// Get recipe content
val content = recipe.content()
println(content)
// Access step images
val stepImages = recipe.stepImages()
stepImages.images.forEach { image ->
println("Section ${image.section}, Step ${image.step}: ${image.imagePath}")
}
} catch (e: CooklangError) {
println("Error: ${e.message}")
}
// Search for recipes
val results = search("./recipes", "chocolate")
results.forEach { recipe ->
println("Found: ${recipe.name()}")
}
// Build a recipe tree
val tree = buildTree("./recipes")
tree.allNodes().forEach { node ->
println("${node.name} - hasRecipe: ${node.hasRecipe}")
}
// Create recipe from content
val content = """
---
title: Quick Omelette
servings: 2
---
Crack @eggs{3} into a bowl and whisk.
""".trimIndent()
val recipe = recipeFromContent(content, null)
println(recipe.name()) // "Quick Omelette"
```
### Error Handling
All functions that can fail throw `CooklangError`:
```kotlin
try {
val recipe = getRecipe(listOf("./recipes"), "nonexistent")
} catch (e: CooklangError.NotFound) {
println("Recipe not found: ${e.message}")
} catch (e: CooklangError.IoError) {
println("IO error: ${e.message}")
} catch (e: CooklangError) {
println("Other error: ${e.message}")
}
```
### ProGuard Rules
If you use ProGuard/R8, the AAR includes consumer rules. If needed manually:
```proguard
-keep class com.sun.jna.** { *; }
-keep class * implements com.sun.jna.** { *; }
-keep class org.cooklang.find.** { *; }
```
## API Reference
### Functions
| `getRecipe(baseDirs, name)` | Load a recipe by name from directories |
| `recipeFromContent(content, name)` | Create a recipe from string content |
| `recipeFromPath(path)` | Create a recipe from a file path |
| `search(baseDir, query)` | Search for recipes matching a query |
| `buildTree(baseDir)` | Build a hierarchical tree of recipes |
| `libraryVersion()` | Get the library version string |
### Types
#### FfiRecipeEntry
| `name()` | `String?` | Recipe name (from title or filename) |
| `path()` | `String?` | File path (if file-backed) |
| `fileName()` | `String?` | File name (if file-backed) |
| `content()` | `String` | Full recipe content |
| `metadata()` | `FfiMetadata` | Recipe metadata |
| `tags()` | `List<String>` | Recipe tags |
| `titleImage()` | `String?` | Title image path/URL |
| `stepImages()` | `FfiStepImages` | Step images |
| `isMenu()` | `Boolean` | Whether this is a .menu file |
| `getStepImage(section, step)` | `String?` | Get specific step image |
| `getMetadataValue(key)` | `String?` | Get metadata value as JSON |
#### FfiMetadata
| `title` | `String?` | Recipe title |
| `servings` | `Long?` | Number of servings |
| `tags` | `List<String>` | Recipe tags |
| `imageUrl` | `String?` | Primary image URL |
| `rawJson` | `String` | Full metadata as JSON |
#### FfiStepImages
| `images` | `List<StepImageEntry>` | All step images |
| `count` | `UInt` | Total image count |
#### StepImageEntry
| `section` | `UInt` | Section number (0 = linear) |
| `step` | `UInt` | Step number (1-indexed) |
| `imagePath` | `String` | Path to image |
#### FfiRecipeTree
| `root()` | `FfiTreeNode` | Root node |
| `allNodes()` | `List<FfiTreeNode>` | All nodes flattened |
| `allRecipes()` | `List<FfiRecipeEntry>` | All recipes |
| `getChild(name)` | `FfiTreeNode?` | Get child by name |
| `recipe()` | `FfiRecipeEntry?` | Recipe at root |
| `getRecipeAtPath(path)` | `FfiRecipeEntry?` | Get recipe by path |
#### FfiTreeNode
| `name` | `String` | Node name |
| `path` | `String` | Full path |
| `hasRecipe` | `Boolean` | Whether node has a recipe |
| `children` | `List<String>` | Child node names |
## CI/CD
The GitHub Actions workflow automatically:
1. Tests the Rust library on push/PR
2. Builds Swift XCFramework (arm64 device + arm64/x86_64 simulator)
3. Builds Android AAR with native libraries (arm64-v8a, armeabi-v7a, x86_64)
4. Creates GitHub Release with all artifacts
5. Updates `Package.swift` with correct checksum for SPM
6. Publishes Android AAR to GitHub Packages Maven repository
To create a release:
```bash
git tag v0.5.2
git push origin v0.5.2
```
This will trigger the workflow to build and publish:
- `CooklangFindFFI.xcframework.zip` - XCFramework binary (used by Package.swift)
- `CooklangFind-ios.zip` - Full iOS package with Swift sources
- `cooklang-find-android.zip` - Android library module with JNI libs
- Android AAR published to `maven.pkg.github.com/cooklang/cooklang-find`