# AshKotlinMultiplatform
[](https://hex.pm/packages/ash_kotlin_multiplatform)
[](https://hexdocs.pm/ash_kotlin_multiplatform)
Generate type-safe Kotlin Multiplatform clients from your Ash resources.
> **Alpha Software Notice**
>
> This library is in **alpha** and under active development. The API may change
> between versions without notice. While functional and tested, it is not yet
> recommended for production use. Please report issues and feedback on
> [GitHub](https://github.com/udin-io/ash_kotlin_multiplatform/issues).
## Features
- **Automatic Kotlin Multiplatform generation** from Elixir Ash resources
- **Swift code generation** for native iOS apps
- **End-to-end type safety** between backend (Elixir) and frontend (Kotlin/Swift)
- **Type-safe RPC client generation** using Ktor (Kotlin) and URLSession (Swift)
- **Built-in RPC controller** for Phoenix applications
- **kotlinx.serialization** integration for JSON handling
- **Swift Codable** integration for native iOS
- **Phoenix Channel support** for real-time applications
- **Kotlin Multiplatform** support (JVM, iOS, JS, Native)
- **Type-safe filtering** with generated filter types
- **Pagination support** for offset and keyset pagination
- **Validation** with optional javax.validation annotations
## Requirements
- Elixir 1.15+
- Ash 3.7+
- Kotlin 1.9+ (for generated code)
- Ktor 2.0+ (for HTTP client)
- kotlinx.serialization 1.6+ (for JSON)
## Installation
Add `ash_kotlin_multiplatform` to your dependencies in `mix.exs`:
```elixir
def deps do
[
{:ash_kotlin_multiplatform, "~> 0.1.0"}
]
end
```
## Quick Start
### 1. Add the Resource Extension
Configure your Ash resources with the `AshKotlinMultiplatform.Resource` extension:
```elixir
defmodule MyApp.Todo do
use Ash.Resource,
domain: MyApp.Domain,
extensions: [AshKotlinMultiplatform.Resource]
kotlin_multiplatform do
# Optional: customize the Kotlin class name
type_name "Todo"
# Optional: map Elixir field names to valid Kotlin identifiers
field_names %{
is_done: :isDone,
created_at: :createdAt
}
end
attributes do
uuid_primary_key :id
attribute :title, :string, allow_nil?: false
attribute :is_done, :boolean, default: false
attribute :created_at, :utc_datetime_usec
end
actions do
defaults [:read, :create, :update, :destroy]
read :list do
pagination offset?: true, default_limit: 25
end
end
end
```
### 2. Add the Domain Extension
Configure your Ash domain with the `AshKotlinMultiplatform.Rpc` extension to expose actions via RPC:
```elixir
defmodule MyApp.Domain do
use Ash.Domain, extensions: [AshKotlinMultiplatform.Rpc]
kotlin_rpc do
resource MyApp.Todo do
# Map RPC endpoint names to Ash actions
rpc_action :list_todos, :list
rpc_action :create_todo, :create
rpc_action :get_todo, :read
rpc_action :update_todo, :update
rpc_action :delete_todo, :destroy
# Optional: define typed queries for common operations
typed_query :active_todos, :list do
filter is_done: false
end
end
end
resources do
resource MyApp.Todo
end
end
```
### 3. Add the RPC Controller
Create a simple RPC controller using the built-in controller module:
```elixir
defmodule MyAppWeb.RpcController do
use MyAppWeb, :controller
use AshKotlinMultiplatform.Phoenix.Controller, otp_app: :my_app
end
```
Add the route to your router:
```elixir
scope "/api" do
pipe_through [:api, :authenticate]
post "/rpc/run", RpcController, :run
post "/rpc/validate", RpcController, :validate
end
```
The controller automatically:
- Discovers RPC actions from your `kotlin_rpc` configuration
- Handles authentication via `conn.assigns[:current_user]`
- Executes actions with proper field formatting
- Returns type-safe JSON responses
**Optional customization:**
```elixir
defmodule MyAppWeb.RpcController do
use MyAppWeb, :controller
use AshKotlinMultiplatform.Phoenix.Controller,
otp_app: :my_app,
actor_key: :current_user,
tenant_key: :tenant,
require_auth: true
# Override actor extraction if needed
def get_actor(conn), do: conn.assigns[:current_user]
end
```
### 4. Generate Kotlin Code
Run the code generation mix task:
```bash
mix ash_kotlin_multiplatform.codegen
```
This generates a Kotlin file (default: `lib/generated/AshRpc.kt`) containing all your types and RPC functions.
### 5. Generate Swift Code (Optional)
For native iOS apps, generate Swift code:
```bash
mix ash_kotlin_multiplatform.swift_codegen
```
Options:
```bash
mix ash_kotlin_multiplatform.swift_codegen \
--output ios/Generated/AshRpc.swift \
--base-url https://api.example.com
```
This generates a Swift file with:
- Codable structs for all resources
- Type-safe RPC service class
- Error handling types
## Generated Code Examples
### Kotlin
The generator produces idiomatic Kotlin code:
```kotlin
package com.myapp.ash
import kotlinx.serialization.*
import kotlinx.datetime.*
import io.ktor.client.*
// Data class with serialization support
@Serializable
data class Todo(
val id: String,
val title: String,
val isDone: Boolean? = false,
val createdAt: Instant? = null
)
// Configuration for actions
@Serializable
data class CreateTodoConfig(
val title: String,
val isDone: Boolean? = null
)
// Sealed class for type-safe results
sealed class CreateTodoResult {
data class Ok(val data: Todo) : CreateTodoResult()
data class Error(val errors: List<RpcError>) : CreateTodoResult()
}
// Functional API
suspend fun createTodo(
client: HttpClient,
config: CreateTodoConfig
): CreateTodoResult { ... }
// Object-oriented API wrapper
object TodoRpc {
suspend fun create(client: HttpClient, config: CreateTodoConfig) =
createTodo(client, config)
suspend fun list(client: HttpClient, config: ListTodosConfig) =
listTodos(client, config)
}
```
### Swift
For native iOS apps, the generator produces Swift code:
```swift
import Foundation
// Codable struct with automatic JSON mapping
struct Todo: Codable, Identifiable {
let id: String
let title: String?
let isDone: Bool?
let createdAt: String?
}
// Actor-based RPC service (thread-safe)
actor AshRpcService {
private let baseURL: String
private var authToken: String?
init(baseURL: String = "http://localhost:4000") {
self.baseURL = baseURL
}
func setAuthToken(_ token: String?) {
self.authToken = token
}
func listTodos(fields: [String]? = nil) async throws -> RpcListResult<Todo> {
// Implementation...
}
func createTodo(input: CreateTodoInput, fields: [String]? = nil) async throws -> RpcResult<Todo> {
// Implementation...
}
}
// Usage in SwiftUI
struct TodoListView: View {
let rpcService = AshRpcService()
@State private var todos: [Todo] = []
var body: some View {
List(todos) { todo in
Text(todo.title ?? "Untitled")
}
.task {
await rpcService.setAuthToken(authManager.token)
if let result = try? await rpcService.listTodos(),
result.success,
let data = result.data {
todos = data
}
}
}
}
```
## Configuration
Configure the generator in your `config/config.exs`:
```elixir
config :ash_kotlin_multiplatform,
# Output settings
output_file: "lib/generated/AshRpc.kt",
swift_output_file: "ios/Generated/AshRpc.swift",
package_name: "com.myapp.ash",
# Code generation options
generate_phoenix_channel_client: true,
generate_validation_functions: true,
generate_filter_types: false,
generate_validation_annotations: false,
# Type handling
datetime_library: :kotlinx_datetime, # or :java_time
nullable_strategy: :explicit, # or :platform
# RPC endpoints
run_endpoint: "/rpc/run",
validate_endpoint: "/rpc/validate",
# Warnings
warn_on_missing_rpc_config: true,
warn_on_non_rpc_references: true
```
### Configuration Options
| Option | Default | Description |
|--------|---------|-------------|
| `output_file` | `"lib/generated/AshRpc.kt"` | Path for generated Kotlin file |
| `swift_output_file` | `"ios/Generated/AshRpc.swift"` | Path for generated Swift file |
| `package_name` | Auto-generated | Kotlin package name |
| `generate_phoenix_channel_client` | `true` | Generate WebSocket client code |
| `generate_validation_functions` | `true` | Generate input validation functions |
| `generate_filter_types` | `false` | Generate type-safe filter types |
| `generate_validation_annotations` | `false` | Add javax.validation annotations |
| `datetime_library` | `:kotlinx_datetime` | Date/time library (`:kotlinx_datetime` or `:java_time`) |
| `nullable_strategy` | `:explicit` | Nullable handling (`:explicit` for `T?`, `:platform` for `T!`) |
| `run_endpoint` | `"/rpc/run"` | RPC execution endpoint |
| `validate_endpoint` | `"/rpc/validate"` | Validation endpoint |
| `type_mapping_overrides` | `[]` | Custom Ash to Kotlin type mappings |
| `input_field_formatter` | `:camel_case` | Input field name format |
| `output_field_formatter` | `:camel_case` | Output field name format |
## DSL Reference
### Resource Extension (`AshKotlinMultiplatform.Resource`)
```elixir
kotlin_multiplatform do
# Custom Kotlin class name (default: resource module name)
type_name "MyCustomName"
# Map field names to valid Kotlin identifiers
field_names %{
elixir_name: :kotlinName
}
# Map argument names per action
argument_names %{
create: %{from_date: :fromDate}
}
end
```
### Domain Extension (`AshKotlinMultiplatform.Rpc`)
```elixir
kotlin_rpc do
resource MyApp.Resource do
# Map RPC actions to Ash actions
rpc_action :rpc_name, :ash_action_name
# Expose metadata fields
rpc_action :list_items, :read do
expose_metadata [:total_count]
end
# Define typed queries with preset filters
typed_query :active_items, :read do
filter status: :active
end
end
end
```
## Phoenix Channel Support
When `generate_phoenix_channel_client: true`, the generator creates a full Phoenix Channel client:
```kotlin
// Connect to Phoenix Channel
val channel = AshRpcChannel(
httpClient = client,
baseUrl = "ws://localhost:4000/socket/websocket"
)
// Join the RPC topic
channel.join("rpc:lobby")
// Make RPC calls over WebSocket
val result = channel.call<CreateTodoResult>("create_todo", config)
```
## Advanced Features
### Type-Safe Filters
Enable with `generate_filter_types: true`:
```kotlin
val filter = TodoFilter(
title = StringFilter(contains = "important"),
isDone = BooleanFilter(eq = false)
)
val result = listTodos(client, ListTodosConfig(filter = filter))
```
### Custom Type Mappings
Map custom Ash types to Kotlin:
```elixir
config :ash_kotlin_multiplatform,
type_mapping_overrides: [
{MyApp.Types.Money, "BigDecimal"},
{MyApp.Types.Email, "String"}
]
```
### Lifecycle Hooks
Add hooks for request processing:
```elixir
config :ash_kotlin_multiplatform,
rpc_action_before_request_hook: "authInterceptor",
rpc_action_after_request_hook: "responseLogger"
```
## Related Packages
- [ash](https://hex.pm/packages/ash) - The Ash Framework
- [ash_phoenix](https://hex.pm/packages/ash_phoenix) - Phoenix integration for Ash
- [ash_json_api](https://hex.pm/packages/ash_json_api) - JSON:API support for Ash
## Contributing
Contributions are welcome! Please open an issue or pull request on [GitHub](https://github.com/udin-io/ash_kotlin_multiplatform).
## License
MIT License - see [LICENSE](LICENSE) for details.