AshKotlinMultiplatform

Hex.pmHex Docs

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

Requirements

Installation

Add ash_kotlin_multiplatform to your dependencies in mix.exs:

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:

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:

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:

defmodule MyAppWeb.RpcController do
  use MyAppWeb, :controller
  use AshKotlinMultiplatform.Phoenix.Controller, otp_app: :my_app
end

Add the route to your router:

scope "/api" do
  pipe_through [:api, :authenticate]
  post "/rpc/run", RpcController, :run
  post "/rpc/validate", RpcController, :validate
end

The controller automatically:

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]
end

4. 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_codegen

Options:

mix ash_kotlin_multiplatform.swift_codegen \
  --output ios/Generated/AshRpc.swift \
  --base-url https://api.example.com

This generates a Swift file with:

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: 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_clienttrue Generate WebSocket client code
generate_validation_functionstrue Generate input validation functions
generate_filter_typesfalse Generate type-safe filter types
generate_validation_annotationsfalse 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
end

Phoenix 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

Contributing

Contributions are welcome! Please open an issue or pull request on GitHub.

License

MIT License - see LICENSE for details.