use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct ExcludeConfig {
#[serde(default)]
pub types: Vec<String>,
#[serde(default)]
pub functions: Vec<String>,
#[serde(default)]
pub methods: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct IncludeConfig {
#[serde(default)]
pub types: Vec<String>,
#[serde(default)]
pub functions: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct OutputConfig {
pub python: Option<PathBuf>,
pub node: Option<PathBuf>,
pub ruby: Option<PathBuf>,
pub php: Option<PathBuf>,
pub elixir: Option<PathBuf>,
pub wasm: Option<PathBuf>,
pub ffi: Option<PathBuf>,
pub go: Option<PathBuf>,
pub java: Option<PathBuf>,
pub kotlin: Option<PathBuf>,
pub kotlin_android: Option<PathBuf>,
pub dart: Option<PathBuf>,
pub swift: Option<PathBuf>,
pub gleam: Option<PathBuf>,
pub csharp: Option<PathBuf>,
pub r: Option<PathBuf>,
pub zig: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ScaffoldConfig {
pub description: Option<String>,
pub license: Option<String>,
pub repository: Option<String>,
pub homepage: Option<String>,
#[serde(default)]
pub authors: Vec<String>,
#[serde(default)]
pub keywords: Vec<String>,
#[serde(default)]
pub generated_header: Option<GeneratedHeaderConfig>,
pub cargo: Option<ScaffoldCargo>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct GeneratedHeaderConfig {
#[serde(default)]
pub issues_url: Option<String>,
#[serde(default)]
pub regenerate_command: Option<String>,
#[serde(default)]
pub verify_command: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ScaffoldCargo {
#[serde(default)]
pub targets: ScaffoldCargoTargets,
#[serde(default = "default_build_jobs")]
pub build_jobs: u32,
#[serde(default)]
pub rustc_wrapper: Option<String>,
#[serde(default)]
pub env: HashMap<String, ScaffoldCargoEnvValue>,
}
impl Default for ScaffoldCargo {
fn default() -> Self {
Self {
targets: ScaffoldCargoTargets::default(),
build_jobs: default_build_jobs(),
rustc_wrapper: None,
env: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ScaffoldCargoTargets {
#[serde(default = "default_true")]
pub macos_dynamic_lookup: bool,
#[serde(default = "default_true")]
pub x86_64_pc_windows_msvc: bool,
#[serde(default = "default_true")]
pub i686_pc_windows_msvc: bool,
#[serde(default = "default_true")]
pub aarch64_unknown_linux_gnu: bool,
#[serde(default = "default_true")]
pub x86_64_unknown_linux_musl: bool,
#[serde(default = "default_true")]
pub wasm32_unknown_unknown: bool,
}
impl Default for ScaffoldCargoTargets {
fn default() -> Self {
Self {
macos_dynamic_lookup: true,
x86_64_pc_windows_msvc: true,
i686_pc_windows_msvc: true,
aarch64_unknown_linux_gnu: true,
x86_64_unknown_linux_musl: true,
wasm32_unknown_unknown: true,
}
}
}
fn default_true() -> bool {
true
}
fn default_build_jobs() -> u32 {
4
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum ScaffoldCargoEnvValue {
Plain(String),
Structured {
value: String,
#[serde(default)]
relative: bool,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ReadmeConfig {
pub template_dir: Option<PathBuf>,
pub snippets_dir: Option<PathBuf>,
pub config: Option<PathBuf>,
pub output_pattern: Option<String>,
pub discord_url: Option<String>,
pub banner_url: Option<String>,
#[serde(default)]
pub languages: HashMap<String, JsonValue>,
#[serde(default)]
pub targets: HashMap<String, JsonValue>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct DocsConfig {
#[serde(default)]
pub reference_output: Option<PathBuf>,
#[serde(default)]
pub cli: Option<DocsSourceConfig>,
#[serde(default)]
pub mcp: Option<DocsSourceConfig>,
#[serde(default)]
pub llms: Option<DocsLlmsConfig>,
#[serde(default)]
pub skills: Option<DocsSkillsConfig>,
#[serde(default)]
pub snippets: Option<DocsSnippetsConfig>,
}
impl DocsConfig {
#[must_use]
pub fn merge(workspace: Option<&Self>, krate: Option<&Self>) -> Option<Self> {
if workspace.is_none() && krate.is_none() {
return None;
}
Some(Self {
reference_output: krate
.and_then(|cfg| cfg.reference_output.clone())
.or_else(|| workspace.and_then(|cfg| cfg.reference_output.clone())),
cli: DocsSourceConfig::merge(
workspace.and_then(|cfg| cfg.cli.as_ref()),
krate.and_then(|cfg| cfg.cli.as_ref()),
),
mcp: DocsSourceConfig::merge(
workspace.and_then(|cfg| cfg.mcp.as_ref()),
krate.and_then(|cfg| cfg.mcp.as_ref()),
),
llms: DocsLlmsConfig::merge(
workspace.and_then(|cfg| cfg.llms.as_ref()),
krate.and_then(|cfg| cfg.llms.as_ref()),
),
skills: DocsSkillsConfig::merge(
workspace.and_then(|cfg| cfg.skills.as_ref()),
krate.and_then(|cfg| cfg.skills.as_ref()),
),
snippets: DocsSnippetsConfig::merge(
workspace.and_then(|cfg| cfg.snippets.as_ref()),
krate.and_then(|cfg| cfg.snippets.as_ref()),
),
})
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct DocsSourceConfig {
#[serde(default)]
pub enabled: Option<bool>,
#[serde(default)]
pub sources: Vec<PathBuf>,
#[serde(default)]
pub output: Option<PathBuf>,
#[serde(default)]
pub adopt_existing: bool,
}
impl DocsSourceConfig {
#[must_use]
pub fn merge(workspace: Option<&Self>, krate: Option<&Self>) -> Option<Self> {
if workspace.is_none() && krate.is_none() {
return None;
}
let sources = krate
.filter(|cfg| !cfg.sources.is_empty())
.map(|cfg| cfg.sources.clone())
.or_else(|| {
workspace
.filter(|cfg| !cfg.sources.is_empty())
.map(|cfg| cfg.sources.clone())
})
.unwrap_or_default();
Some(Self {
enabled: krate
.and_then(|cfg| cfg.enabled)
.or_else(|| workspace.and_then(|cfg| cfg.enabled)),
sources,
output: krate
.and_then(|cfg| cfg.output.clone())
.or_else(|| workspace.and_then(|cfg| cfg.output.clone())),
adopt_existing: krate
.map(|cfg| cfg.adopt_existing)
.unwrap_or_else(|| workspace.map(|cfg| cfg.adopt_existing).unwrap_or(false)),
})
}
#[must_use]
pub fn is_enabled(&self) -> bool {
self.enabled.unwrap_or(true)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct DocsLlmsConfig {
#[serde(default)]
pub template: Option<PathBuf>,
#[serde(default)]
pub output: Option<PathBuf>,
#[serde(default)]
pub adopt_existing: bool,
}
impl DocsLlmsConfig {
#[must_use]
pub fn merge(workspace: Option<&Self>, krate: Option<&Self>) -> Option<Self> {
if workspace.is_none() && krate.is_none() {
return None;
}
Some(Self {
template: krate
.and_then(|cfg| cfg.template.clone())
.or_else(|| workspace.and_then(|cfg| cfg.template.clone())),
output: krate
.and_then(|cfg| cfg.output.clone())
.or_else(|| workspace.and_then(|cfg| cfg.output.clone())),
adopt_existing: krate
.map(|cfg| cfg.adopt_existing)
.unwrap_or_else(|| workspace.map(|cfg| cfg.adopt_existing).unwrap_or(false)),
})
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct DocsSkillsConfig {
#[serde(default)]
pub template_dir: Option<PathBuf>,
#[serde(default)]
pub outputs: Vec<PathBuf>,
#[serde(default)]
pub templates: HashMap<String, DocsSkillTemplateConfig>,
#[serde(default)]
pub adopt_existing: bool,
}
impl DocsSkillsConfig {
#[must_use]
pub fn merge(workspace: Option<&Self>, krate: Option<&Self>) -> Option<Self> {
if workspace.is_none() && krate.is_none() {
return None;
}
let outputs = krate
.filter(|cfg| !cfg.outputs.is_empty())
.map(|cfg| cfg.outputs.clone())
.or_else(|| {
workspace
.filter(|cfg| !cfg.outputs.is_empty())
.map(|cfg| cfg.outputs.clone())
})
.unwrap_or_default();
let mut templates = workspace.map(|cfg| cfg.templates.clone()).unwrap_or_default();
if let Some(krate) = krate {
templates.extend(krate.templates.clone());
}
Some(Self {
template_dir: krate
.and_then(|cfg| cfg.template_dir.clone())
.or_else(|| workspace.and_then(|cfg| cfg.template_dir.clone())),
outputs,
templates,
adopt_existing: krate
.map(|cfg| cfg.adopt_existing)
.unwrap_or_else(|| workspace.map(|cfg| cfg.adopt_existing).unwrap_or(false)),
})
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct DocsSkillTemplateConfig {
#[serde(default)]
pub template: Option<PathBuf>,
#[serde(default)]
pub output: Option<PathBuf>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct DocsSnippetsConfig {
#[serde(default)]
pub dirs: Vec<PathBuf>,
#[serde(default)]
pub docs_dirs: Vec<PathBuf>,
#[serde(default)]
pub required_languages: Vec<String>,
#[serde(default)]
pub include_base_paths: Vec<PathBuf>,
#[serde(default)]
pub require_frontmatter: bool,
#[serde(default)]
pub validation_level: Option<String>,
#[serde(default)]
pub timeout_secs: Option<u64>,
#[serde(default)]
pub fail_fast: bool,
}
impl DocsSnippetsConfig {
#[must_use]
pub fn merge(workspace: Option<&Self>, krate: Option<&Self>) -> Option<Self> {
if workspace.is_none() && krate.is_none() {
return None;
}
Some(Self {
dirs: merge_vec(workspace.map(|cfg| &cfg.dirs), krate.map(|cfg| &cfg.dirs)),
docs_dirs: merge_vec(workspace.map(|cfg| &cfg.docs_dirs), krate.map(|cfg| &cfg.docs_dirs)),
required_languages: merge_vec(
workspace.map(|cfg| &cfg.required_languages),
krate.map(|cfg| &cfg.required_languages),
),
include_base_paths: merge_vec(
workspace.map(|cfg| &cfg.include_base_paths),
krate.map(|cfg| &cfg.include_base_paths),
),
require_frontmatter: krate
.map(|cfg| cfg.require_frontmatter)
.unwrap_or_else(|| workspace.map(|cfg| cfg.require_frontmatter).unwrap_or(false)),
validation_level: krate
.and_then(|cfg| cfg.validation_level.clone())
.or_else(|| workspace.and_then(|cfg| cfg.validation_level.clone())),
timeout_secs: krate
.and_then(|cfg| cfg.timeout_secs)
.or_else(|| workspace.and_then(|cfg| cfg.timeout_secs)),
fail_fast: krate
.map(|cfg| cfg.fail_fast)
.unwrap_or_else(|| workspace.map(|cfg| cfg.fail_fast).unwrap_or(false)),
})
}
}
fn merge_vec<T: Clone>(workspace: Option<&Vec<T>>, krate: Option<&Vec<T>>) -> Vec<T> {
krate
.filter(|items| !items.is_empty())
.cloned()
.or_else(|| workspace.filter(|items| !items.is_empty()).cloned())
.unwrap_or_default()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(untagged)]
pub enum StringOrVec {
Single(String),
Multiple(Vec<String>),
}
impl StringOrVec {
pub fn commands(&self) -> Vec<&str> {
match self {
StringOrVec::Single(s) => vec![s.as_str()],
StringOrVec::Multiple(v) => v.iter().map(String::as_str).collect(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct LintConfig {
pub precondition: Option<String>,
pub before: Option<StringOrVec>,
pub format: Option<StringOrVec>,
pub check: Option<StringOrVec>,
pub typecheck: Option<StringOrVec>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct UpdateConfig {
pub precondition: Option<String>,
pub before: Option<StringOrVec>,
pub update: Option<StringOrVec>,
pub upgrade: Option<StringOrVec>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct TestAppRunConfig {
pub precondition: Option<String>,
pub before: Option<StringOrVec>,
pub run: Option<StringOrVec>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, JsonSchema)]
pub struct TestConfig {
pub precondition: Option<String>,
pub before: Option<StringOrVec>,
pub command: Option<StringOrVec>,
pub e2e: Option<StringOrVec>,
pub coverage: Option<StringOrVec>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct SetupConfig {
pub precondition: Option<String>,
pub before: Option<StringOrVec>,
pub install: Option<StringOrVec>,
#[serde(default = "default_setup_timeout")]
pub timeout_seconds: u64,
#[serde(default)]
pub workdir: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct CleanConfig {
pub precondition: Option<String>,
pub before: Option<StringOrVec>,
pub clean: Option<StringOrVec>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct BuildCommandConfig {
pub precondition: Option<String>,
pub before: Option<StringOrVec>,
pub build: Option<StringOrVec>,
pub build_release: Option<StringOrVec>,
}
impl BuildCommandConfig {
pub fn merge_overlay(mut self, other: &Self) -> Self {
if other.precondition.is_some() {
self.precondition = other.precondition.clone();
}
if other.before.is_some() {
self.before = other.before.clone();
}
if other.build.is_some() {
self.build = other.build.clone();
}
if other.build_release.is_some() {
self.build_release = other.build_release.clone();
}
self
}
}
fn default_setup_timeout() -> u64 {
1800
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct OutputTemplate {
pub python: Option<String>,
pub node: Option<String>,
pub ruby: Option<String>,
pub php: Option<String>,
pub elixir: Option<String>,
pub wasm: Option<String>,
pub ffi: Option<String>,
pub go: Option<String>,
pub java: Option<String>,
pub kotlin: Option<String>,
pub kotlin_android: Option<String>,
pub dart: Option<String>,
pub swift: Option<String>,
pub gleam: Option<String>,
pub csharp: Option<String>,
pub r: Option<String>,
pub zig: Option<String>,
}
impl OutputTemplate {
pub fn resolve(&self, crate_name: &str, lang: &str, multi_crate: bool) -> PathBuf {
validate_output_segment(crate_name, "crate_name");
validate_output_segment(lang, "lang");
let path = if let Some(template) = self.entry(lang) {
PathBuf::from(template.replace("{crate}", crate_name).replace("{lang}", lang))
} else if multi_crate {
PathBuf::from(format!("packages/{lang}/{crate_name}"))
} else {
match lang {
"python" => PathBuf::from("packages/python"),
"node" => PathBuf::from("packages/node"),
"ruby" => PathBuf::from("packages/ruby"),
"php" => PathBuf::from("packages/php"),
"elixir" => PathBuf::from("packages/elixir"),
other => PathBuf::from(format!("packages/{other}")),
}
};
validate_output_path(&path);
path
}
pub fn entry(&self, lang: &str) -> Option<&str> {
match lang {
"python" => self.python.as_deref(),
"node" => self.node.as_deref(),
"ruby" => self.ruby.as_deref(),
"php" => self.php.as_deref(),
"elixir" => self.elixir.as_deref(),
"wasm" => self.wasm.as_deref(),
"ffi" => self.ffi.as_deref(),
"go" => self.go.as_deref(),
"java" => self.java.as_deref(),
"kotlin" => self.kotlin.as_deref(),
"kotlin_android" => self.kotlin_android.as_deref(),
"dart" => self.dart.as_deref(),
"swift" => self.swift.as_deref(),
"gleam" => self.gleam.as_deref(),
"csharp" => self.csharp.as_deref(),
"r" => self.r.as_deref(),
"zig" => self.zig.as_deref(),
_ => None,
}
}
}
fn validate_output_segment(segment: &str, label: &str) {
if segment.contains('\0') {
panic!("invalid {label}: NUL byte is not allowed in output path segments (got {segment:?})");
}
if segment.contains('/') || segment.contains('\\') {
panic!("invalid {label}: path separators are not allowed in output path segments (got {segment:?})");
}
}
fn validate_output_path(path: &std::path::Path) {
use std::path::Component;
for component in path.components() {
match component {
Component::ParentDir => {
panic!(
"resolved output path `{}` contains `..` and would escape the project root",
path.display()
);
}
Component::RootDir | Component::Prefix(_) => {
panic!(
"resolved output path `{}` is absolute and would escape the project root",
path.display()
);
}
_ => {}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TextReplacement {
pub path: String,
pub search: String,
pub replace: String,
}
#[cfg(test)]
mod tests;
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct SyncConfig {
#[serde(default)]
pub extra_paths: Vec<String>,
#[serde(default)]
pub text_replacements: Vec<TextReplacement>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CitationAuthor {
#[serde(default, alias = "family-names")]
pub family_names: Option<String>,
#[serde(default, alias = "given-names")]
pub given_names: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub email: Option<String>,
#[serde(default)]
pub orcid: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CitationConfig {
pub title: String,
#[serde(rename = "abstract")]
pub abstract_: String,
pub authors: Vec<CitationAuthor>,
#[serde(default = "default_citation_message")]
pub message: String,
#[serde(rename = "repository-code", alias = "repository_code")]
pub repository_code: String,
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub license: Option<String>,
#[serde(default, rename = "date-released", alias = "date_released")]
pub date_released: Option<String>,
#[serde(default)]
pub doi: Option<String>,
}
fn default_citation_message() -> String {
"If you use this software, please cite it using the metadata below.".to_string()
}