#![expect(
clippy::needless_for_each,
reason = "noise from inside utoipa's OpenApi derive expansion"
)]
use schemars::schema_for;
use utoipa::OpenApi;
use crate::config::file::Config;
#[must_use]
pub fn config_json_schema() -> serde_json::Value {
let schema = schema_for!(Config);
serde_json::to_value(&schema).expect("JSON Schema is always serialisable")
}
#[derive(OpenApi)]
#[openapi(
info(
title = "Martin",
description = "Blazing-fast tile server with PostGIS, MBTiles, and PMTiles support.",
license(name = "MIT OR Apache-2.0"),
),
paths(
crate::srv::get_health,
crate::srv::get_catalog,
crate::srv::get_source_info,
crate::srv::get_tile,
crate::srv::get_sprite_png,
crate::srv::get_sprite_sdf_png,
crate::srv::get_sprite_json,
crate::srv::get_sprite_sdf_json,
crate::srv::get_font,
crate::srv::get_style_json,
)
)]
pub struct MartinOpenApi;
#[cfg(all(feature = "rendering", target_os = "linux"))]
#[derive(OpenApi)]
#[openapi(paths(crate::srv::get_style_rendered))]
struct MartinRenderingOpenApi;
#[must_use]
pub fn openapi_spec() -> serde_json::Value {
let openapi = MartinOpenApi::openapi();
#[cfg(all(feature = "rendering", target_os = "linux"))]
let openapi = openapi.merge_from(MartinRenderingOpenApi::openapi());
serde_json::to_value(&openapi).expect("OpenAPI doc is always serialisable")
}
#[must_use]
pub fn config_doc_yaml() -> String {
let schema = config_json_schema();
let mut out = String::new();
out.push_str(
"<!-- This file is auto-generated by `just gen-schemas`. \
Don't edit it, edit the code's doc comments instead -->\n\n",
);
out.push_str("Schema: [`schemas/config.json`](https://github.com/maplibre/martin/blob/main/schemas/config.json).\n\n");
out.push_str("```yaml title=\"config.yaml\"\n");
out.push_str(
"# yaml-language-server: $schema=https://raw.githubusercontent.com/maplibre/martin/main/schemas/config.json\n",
);
let ctx = config_doc::Ctx::new(&schema);
config_doc::render_object(&mut out, &schema, &ctx, 0);
out.push_str("\n```\n");
out
}
mod config_doc {
use std::fmt::Write as _;
use serde_json::Value;
pub(super) struct Ctx<'a> {
defs: Option<&'a serde_json::Map<String, Value>>,
}
impl<'a> Ctx<'a> {
pub(super) fn new(root: &'a Value) -> Self {
let defs = root.get("$defs").and_then(Value::as_object);
Self { defs }
}
pub(super) fn resolve(&self, schema: &'a Value) -> &'a Value {
let Some(reference) = schema.get("$ref").and_then(Value::as_str) else {
return schema;
};
let Some(name) = reference.rsplit('/').next() else {
return schema;
};
self.defs.and_then(|d| d.get(name)).map_or(schema, |v| v)
}
}
pub(super) fn render_object(out: &mut String, schema: &Value, ctx: &Ctx, indent: usize) {
let resolved = ctx.resolve(schema);
let Some(properties) = resolved.get("properties").and_then(Value::as_object) else {
return;
};
let mut first = true;
for (key, prop) in properties {
if !first {
out.push('\n');
}
first = false;
if let Some(desc) = prop.get("description").and_then(Value::as_str) {
emit_comment(out, desc, indent);
}
push_indent(out, indent);
out.push_str(key);
out.push(':');
render_value(out, prop, ctx, indent);
}
}
fn emit_comment(out: &mut String, desc: &str, indent: usize) {
for line in desc.lines() {
push_indent(out, indent);
if line.is_empty() {
out.push('#');
} else {
out.push_str("# ");
push_unescaped(out, line);
}
out.push('\n');
}
}
fn push_unescaped(out: &mut String, line: &str) {
let mut chars = line.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\'
&& let Some(&next) = chars.peek()
&& (next == '[' || next == ']')
{
out.push(next);
chars.next();
} else {
out.push(c);
}
}
}
fn render_value(out: &mut String, schema: &Value, ctx: &Ctx, indent: usize) {
let schema = ctx.resolve(schema);
if let Some(example) = first_example(schema) {
emit_inline_or_block(out, example, indent);
return;
}
if let Some(default) = schema.get("default")
&& !default.is_null()
{
emit_inline_or_block(out, default, indent);
return;
}
if let Some(values) = schema.get("enum").and_then(Value::as_array)
&& let Some(first) = values.first()
{
emit_inline_or_block(out, first, indent);
return;
}
if let Some(variants) = schema.get("oneOf").and_then(Value::as_array)
&& let Some(constant) = variants
.iter()
.find_map(|v| ctx.resolve(v).get("const").cloned())
{
emit_inline_or_block(out, &constant, indent);
return;
}
if let Some(variants) = schema.get("anyOf").or_else(|| schema.get("oneOf"))
&& let Some(variants) = variants.as_array()
{
let primary = pick_primary_variant(variants, ctx);
render_value(out, primary, ctx, indent);
return;
}
let nullable = is_nullable(schema);
match primary_type(schema).as_deref() {
Some("object") => {
if schema.get("properties").is_some() {
out.push('\n');
render_object(out, schema, ctx, indent + 1);
} else {
out.push_str(" {}");
}
}
Some("array") => out.push_str(" []"),
_ if nullable => out.push_str(" null"),
Some("string") => out.push_str(" \"\""),
Some("integer" | "number") => out.push_str(" 0"),
Some("boolean") => out.push_str(" false"),
_ => out.push_str(" null"),
}
}
fn is_nullable(schema: &Value) -> bool {
match schema.get("type") {
Some(Value::Array(a)) => a.iter().any(|v| v.as_str() == Some("null")),
_ => false,
}
}
fn pick_primary_variant<'a>(variants: &'a [Value], ctx: &'a Ctx) -> &'a Value {
fn rank(schema: &Value) -> u8 {
if schema.get("anyOf").is_some() || schema.get("oneOf").is_some() {
return 6;
}
match primary_type(schema).as_deref() {
Some("object") => 5,
Some("array") => 4,
Some("integer" | "number") => 3,
Some("boolean") => 2,
Some("string") => 1,
_ => 0,
}
}
variants
.iter()
.max_by_key(|v| rank(ctx.resolve(v)))
.unwrap_or(&variants[0])
}
fn emit_inline_or_block(out: &mut String, value: &Value, indent: usize) {
match value {
Value::Object(_) | Value::Array(_) if !is_empty_collection(value) => {
let yaml = serde_yaml::to_string(value).unwrap_or_default();
for line in yaml.lines() {
out.push('\n');
push_indent(out, indent + 1);
out.push_str(line);
}
}
_ => {
out.push(' ');
let yaml = serde_yaml::to_string(value).unwrap_or_default();
out.push_str(yaml.trim_end());
}
}
}
fn is_empty_collection(v: &Value) -> bool {
matches!(v, Value::Object(o) if o.is_empty())
|| matches!(v, Value::Array(a) if a.is_empty())
}
fn first_example(schema: &Value) -> Option<&Value> {
schema
.get("examples")
.and_then(Value::as_array)
.and_then(|a| a.first())
}
fn primary_type(schema: &Value) -> Option<String> {
match schema.get("type")? {
Value::String(s) => Some(s.clone()),
Value::Array(a) => a
.iter()
.filter_map(Value::as_str)
.find(|t| *t != "null")
.or_else(|| a.iter().find_map(Value::as_str))
.map(str::to_owned),
_ => None,
}
}
fn push_indent(out: &mut String, indent: usize) {
for _ in 0..indent {
let _ = write!(out, " ");
}
}
}