AshKotlinMultiplatform
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.
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:
def deps do
[
{:ash_kotlin_multiplatform, "~> 0.1.0"}
]
endQuick Start
1. Add the Resource Extension
Configure your Ash resources with the AshKotlinMultiplatform.Resource extension:
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
end2. Add the Domain Extension
Configure your Ash domain with the AshKotlinMultiplatform.Rpc extension to expose actions via RPC:
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
end3. Add the RPC Controller
Create a simple RPC controller using the built-in controller module:
defmodule MyAppWeb.RpcController do
use MyAppWeb, :controller
use AshKotlinMultiplatform.Phoenix.Controller, otp_app: :my_app
endAdd the route to your router:
scope "/api" do
pipe_through [:api, :authenticate]
post "/rpc/run", RpcController, :run
post "/rpc/validate", RpcController, :validate
endThe controller automatically:
-
Discovers RPC actions from your
kotlin_rpcconfiguration -
Handles authentication via
conn.assigns[:current_user] - Executes actions with proper field formatting
- Returns type-safe JSON responses
Optional customization:
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]
end4. Generate Kotlin Code
Run the code generation mix task:
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:
mix ash_kotlin_multiplatform.swift_codegenOptions:
mix ash_kotlin_multiplatform.swift_codegen \
--output ios/Generated/AshRpc.swift \
--base-url https://api.example.comThis 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:
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:
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:
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: trueConfiguration 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)
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)
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
endPhoenix Channel Support
When generate_phoenix_channel_client: true, the generator creates a full Phoenix Channel client:
// 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:
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:
config :ash_kotlin_multiplatform,
type_mapping_overrides: [
{MyApp.Types.Money, "BigDecimal"},
{MyApp.Types.Email, "String"}
]Lifecycle Hooks
Add hooks for request processing:
config :ash_kotlin_multiplatform,
rpc_action_before_request_hook: "authInterceptor",
rpc_action_after_request_hook: "responseLogger"Related Packages
- ash - The Ash Framework
- ash_phoenix - Phoenix integration for Ash
- ash_json_api - JSON:API support for Ash
Contributing
Contributions are welcome! Please open an issue or pull request on GitHub.
License
MIT License - see LICENSE for details.