use std::collections::{BTreeMap, HashMap};
use concinnity_core::ecs::schema::{
self, Body, FieldSchema, TypeSchema, ValueSchema, VariantSchema,
};
use super::defaults::{Defaults, default_text};
use super::prose::{
collapse_doc, entry_examples_as_lines, first_paragraph, strip_rust_blocks, strip_table_lines,
};
use super::render::{
EnumValue, FieldEntry, FieldType, render_parameters, render_values, rewrite_doc_links, slug,
};
pub(super) struct AssetDoc {
pub(super) type_name: String,
pub(super) summary: String,
pub(super) full_doc: String,
pub(super) is_reference_type: bool,
}
pub(super) struct AssetEntry {
pub(super) name: &'static str,
pub(super) schema: &'static TypeSchema,
}
pub(super) fn registry_assets() -> Vec<AssetEntry> {
use concinnity_cook::authoring::registry::{AssetOrigin, RegisteredType};
RegisteredType::all()
.iter()
.filter(|ty| ty.registration().origin != AssetOrigin::RuntimeOnly)
.map(|ty| AssetEntry {
name: ty.as_str(),
schema: ty.schema(),
})
.collect()
}
pub(super) fn build(assets: &[AssetEntry]) -> Vec<AssetDoc> {
let mut ctx = Ctx {
asset_by_schema: assets.iter().map(|a| (a.schema.name, a.name)).collect(),
value_types: BTreeMap::new(),
enums: BTreeMap::new(),
};
let mut asset_docs: Vec<Entry> = assets
.iter()
.map(|a| {
let (summary, full_doc) = render_struct(a.schema, &mut ctx);
Entry {
name: a.name.to_string(),
summary,
full_doc,
}
})
.collect();
let mut ref_types: Vec<Entry> = Vec::new();
let mut done: Vec<&str> = Vec::new();
loop {
let pending: Vec<&'static TypeSchema> = ctx
.value_types
.values()
.filter(|s| !done.contains(&s.name))
.copied()
.collect();
if pending.is_empty() {
break;
}
for s in pending {
done.push(s.name);
let (summary, full_doc) = render_struct(s, &mut ctx);
ref_types.push(Entry {
name: s.name.to_string(),
summary: or_fallback(summary, "Nested object embedded by other assets."),
full_doc,
});
}
}
for s in ctx.enums.values() {
let (summary, full_doc) = render_enum(s);
ref_types.push(Entry {
name: s.name.to_string(),
summary: or_fallback(summary, "A set of named string values."),
full_doc,
});
}
let name_for_slug: HashMap<String, String> = asset_docs
.iter()
.chain(ref_types.iter())
.map(|e| (slug(&e.name), e.name.clone()))
.collect();
for e in asset_docs.iter_mut().chain(ref_types.iter_mut()) {
e.summary = rewrite_doc_links(&e.summary, &name_for_slug);
e.full_doc = rewrite_doc_links(&e.full_doc, &name_for_slug);
}
asset_docs.sort_by(|a, b| a.name.cmp(&b.name));
ref_types.sort_by(|a, b| a.name.cmp(&b.name));
let docs = |entries: Vec<Entry>, is_reference_type| {
entries.into_iter().map(move |e| AssetDoc {
type_name: e.name,
summary: e.summary,
full_doc: e.full_doc,
is_reference_type,
})
};
docs(asset_docs, false)
.chain(docs(ref_types, true))
.collect()
}
struct Ctx {
asset_by_schema: HashMap<&'static str, &'static str>,
value_types: BTreeMap<&'static str, &'static TypeSchema>,
enums: BTreeMap<&'static str, &'static TypeSchema>,
}
struct Entry {
name: String,
summary: String,
full_doc: String,
}
fn or_fallback(summary: String, fallback: &str) -> String {
if summary.is_empty() {
fallback.to_string()
} else {
summary
}
}
fn render_struct(s: &'static TypeSchema, ctx: &mut Ctx) -> (String, String) {
let doc = entry_examples_as_lines(&strip_rust_blocks(s.doc));
let mut fields = Vec::new();
collect_fields(s, ctx, &mut fields);
let full_doc = combine(&strip_table_lines(&doc), &render_parameters(&fields));
(first_paragraph(&doc), full_doc)
}
fn render_enum(s: &TypeSchema) -> (String, String) {
let values: Vec<EnumValue> = match s.body {
Body::Variants(variants) => variants.iter().map(variant_value).collect(),
_ => enum_values(s)
.iter()
.map(|v| EnumValue {
value: v.name.to_string(),
doc: collapse_doc(v.doc),
})
.collect(),
};
let cleaned = strip_table_lines(&strip_rust_blocks(s.doc));
(
first_paragraph(s.doc),
combine(&cleaned, &render_values(&values)),
)
}
fn combine(description: &str, section: &str) -> String {
match (description.is_empty(), section.is_empty()) {
(_, true) => description.to_string(),
(true, false) => section.to_string(),
(false, false) => format!("{}\n\n{}", description, section.trim_end()),
}
}
fn collect_fields(s: &'static TypeSchema, ctx: &mut Ctx, out: &mut Vec<FieldEntry>) {
let Body::Fields(fields) = s.body else {
return;
};
let defaults = Defaults::of(s);
for field in fields {
if field.is_flattened() {
if let schema::FieldType::Nested(inner) = (field.ty)() {
collect_fields(inner, ctx, out);
}
continue;
}
out.push(field_entry(field, &defaults, ctx));
}
}
fn field_entry(field: &FieldSchema, defaults: &Defaults, ctx: &mut Ctx) -> FieldEntry {
let ty = (field.ty)();
FieldEntry {
key: field.key.to_string(),
optional: matches!(ty, schema::FieldType::Optional(_)),
default: defaults
.field(field)
.filter(|value| states_something(unwrap_optional(ty), value))
.map(|value| default_text(&value)),
ty: resolve_type(ty, ctx),
doc: collapse_doc(field.doc),
}
}
fn states_something(ty: schema::FieldType, value: &serde_json::Value) -> bool {
match ty {
schema::FieldType::Reference(_) => value.as_str() != Some(""),
schema::FieldType::Nested(inner) => Defaults::of(inner).container() != Some(value),
_ => true,
}
}
fn unwrap_optional(ty: schema::FieldType) -> schema::FieldType {
match ty {
schema::FieldType::Optional(inner) => unwrap_optional(inner()),
other => other,
}
}
fn resolve_type(ty: schema::FieldType, ctx: &mut Ctx) -> FieldType {
match ty {
schema::FieldType::Bool => FieldType::Bool,
schema::FieldType::Float => FieldType::Float,
schema::FieldType::Integer => FieldType::Integer,
schema::FieldType::Str | schema::FieldType::Reference(_) => FieldType::Str,
schema::FieldType::Object => FieldType::Object,
schema::FieldType::Optional(inner) => resolve_type(inner(), ctx),
schema::FieldType::Array { elem, len } => FieldType::Array {
elem: Box::new(resolve_type(elem(), ctx)),
len,
},
schema::FieldType::Nested(s) => match ctx.asset_by_schema.get(s.name) {
Some(asset) => FieldType::Named(asset.to_string()),
None => {
ctx.value_types.entry(s.name).or_insert(s);
FieldType::Named(s.name.to_string())
}
},
schema::FieldType::Enum(s) if matches!(s.body, Body::Variants(_)) => {
if !ctx.enums.contains_key(s.name) {
ctx.enums.insert(s.name, s);
reach_payloads(s, ctx);
}
FieldType::Tagged(s.name.to_string())
}
schema::FieldType::Enum(s) => {
if enum_is_documented(s) {
ctx.enums.entry(s.name).or_insert(s);
FieldType::NamedEnum(s.name.to_string())
} else {
FieldType::Enum(enum_values(s).iter().map(|v| v.name.to_string()).collect())
}
}
}
}
fn enum_values(s: &TypeSchema) -> &'static [ValueSchema] {
match s.body {
Body::Values(values) => values,
Body::Fields(_) | Body::Variants(_) => &[],
}
}
fn enum_is_documented(s: &TypeSchema) -> bool {
!s.doc.trim().is_empty() || enum_values(s).iter().any(|v| !v.doc.trim().is_empty())
}
fn variant_value(v: &VariantSchema) -> EnumValue {
let mut doc = collapse_doc(v.doc);
let Some(payload) = v.payload else {
return EnumValue {
value: format!("\"{}\"", v.name),
doc,
};
};
let placeholder = match payload() {
schema::FieldType::Reference(targets) if !targets.is_empty() => {
format!("\"<{} name>\"", targets.join(" or "))
}
schema::FieldType::Reference(_) | schema::FieldType::Str => "\"<name>\"".to_string(),
schema::FieldType::Integer => "<integer>".to_string(),
schema::FieldType::Float => "<number>".to_string(),
schema::FieldType::Enum(inner) => {
doc = format!("{doc} See [{0}]({0}.md).", inner.name);
format!("<{}>", inner.name)
}
_ => "{...}".to_string(),
};
EnumValue {
value: format!("{{\"{}\": {placeholder}}}", v.name),
doc,
}
}
fn reach_payloads(s: &'static TypeSchema, ctx: &mut Ctx) {
let Body::Variants(variants) = s.body else {
return;
};
for payload in variants.iter().filter_map(|v| v.payload) {
if let schema::FieldType::Enum(inner) = payload()
&& !ctx.enums.contains_key(inner.name)
{
ctx.enums.insert(inner.name, inner);
reach_payloads(inner, ctx);
}
}
}