Packages

An OTP-native coding agent SDK. Build, orchestrate, and observe AI coding agents with Elixir supervision trees, streaming events, and a JSON-RPC 2.0 interface.

Current section

Files

Jump to
opal lib mix tasks opal.gen.json_schema.ex
Raw

lib/mix/tasks/opal.gen.json_schema.ex

defmodule Mix.Tasks.Opal.Gen.JsonSchema do
@moduledoc """
Generates a JSON Schema file from the Opal RPC protocol specification.
The output is written to `priv/rpc_schema.json` by default, or to the
path given as the first argument.
## Usage
mix opal.gen.json_schema
mix opal.gen.json_schema ../sdk/src/rpc_schema.json
"""
use Mix.Task
@shortdoc "Generate JSON Schema from Opal.RPC.Protocol"
@impl true
def run(args) do
output_path = List.first(args) || "priv/rpc_schema.json"
spec = Opal.RPC.Protocol.spec()
schema = %{
"$schema" => "http://json-schema.org/draft-07/schema#",
"title" => "Opal RPC Protocol",
"description" => "Auto-generated from Opal.RPC.Protocol",
"version" => spec.version,
"definitions" => %{
"methods" => build_methods(spec.methods),
"server_requests" => build_methods(spec.server_requests),
"events" => build_events(spec.event_types),
"notification_method" => spec.notification_method
}
}
output_path |> Path.dirname() |> File.mkdir_p!()
json = Jason.encode!(schema, pretty: true)
File.write!(output_path, json <> "\n")
Mix.shell().info("Generated JSON Schema → #{output_path}")
end
# -- Methods / Server Requests --
defp build_methods(methods) do
Map.new(methods, fn m ->
params_schema = params_to_json_schema(m.params)
result_schema = fields_to_json_schema(m.result)
{m.method, %{
"description" => m.description,
"params" => params_schema,
"result" => result_schema
}}
end)
end
defp params_to_json_schema(params) do
properties = Map.new(params, fn p -> {p.name, type_to_json_schema(p.type, p.description)} end)
required = params |> Enum.filter(& &1.required) |> Enum.map(& &1.name)
schema = %{"type" => "object", "properties" => properties}
if required == [], do: schema, else: Map.put(schema, "required", required)
end
defp fields_to_json_schema(fields) do
properties = Map.new(fields, fn f -> {f.name, type_to_json_schema(f.type, f.description)} end)
required = Enum.map(fields, & &1.name)
schema = %{"type" => "object", "properties" => properties}
if required == [], do: schema, else: Map.put(schema, "required", required)
end
# -- Events --
defp build_events(event_types) do
Map.new(event_types, fn e ->
fields_schema = fields_to_json_schema(e.fields)
base = %{
"type" => "object",
"properties" => Map.merge(
%{
"type" => %{"type" => "string", "const" => e.type, "description" => "Event type discriminator."},
"session_id" => %{"type" => "string", "description" => "Session this event belongs to."}
},
fields_schema["properties"] || %{}
),
"required" => ["type", "session_id"] ++ (fields_schema["required"] || []),
"description" => e.description
}
{e.type, base}
end)
end
# -- Type String → JSON Schema --
@doc false
def type_to_json_schema(type_str, description \\ "") do
schema = parse_type(type_str)
if description != "", do: Map.put(schema, "description", description), else: schema
end
defp parse_type("string"), do: %{"type" => "string"}
defp parse_type("boolean"), do: %{"type" => "boolean"}
defp parse_type("integer"), do: %{"type" => "integer"}
defp parse_type("number"), do: %{"type" => "number"}
defp parse_type("object"), do: %{"type" => "object"}
defp parse_type("string[]"), do: %{"type" => "array", "items" => %{"type" => "string"}}
defp parse_type("object[]"), do: %{"type" => "array", "items" => %{"type" => "object"}}
defp parse_type("object{" <> rest) do
# Parse inline object type like "object{provider:string, id:string}"
# Supports optional fields with ? suffix: "object{ok:boolean, output?:string}"
fields_str = String.trim_trailing(rest, "}")
pairs =
fields_str
|> String.split(",")
|> Enum.map(fn pair ->
[name, type] = pair |> String.trim() |> String.split(":", parts: 2)
name = String.trim(name)
type = String.trim(type)
{optional?, clean_name} =
if String.ends_with?(name, "?") do
{true, String.trim_trailing(name, "?")}
else
{false, name}
end
{clean_name, parse_type(type), optional?}
end)
properties = Map.new(pairs, fn {name, schema, _} -> {name, schema} end)
required = pairs |> Enum.reject(fn {_, _, opt?} -> opt? end) |> Enum.map(fn {name, _, _} -> name end)
schema = %{"type" => "object", "properties" => properties}
if required == [], do: schema, else: Map.put(schema, "required", required)
end
# Fallback for types we can't parse — keep as-is in description
defp parse_type(other), do: %{"type" => "object", "description" => "Complex type: #{other}"}
end