1
0
Fork 0
forked from wry/wry

config: change default config to use toml-based configuration

This commit is contained in:
Julian Orth 2024-03-13 19:30:34 +01:00
parent e24a61bc62
commit 3cebf651c5
58 changed files with 14093 additions and 145 deletions

View file

@ -0,0 +1,194 @@
use {
crate::types::{
ArraySpec, Described, MapSpec, NestableTypesSpec, NumberSpec, RefOrSpec, SingleTableSpec,
StringSpec, TableSpec, TopLevelTypeSpec, VariantSpec,
},
anyhow::Result,
serde_json::{json, Map, Value},
};
pub fn generate_json_schema(
types_sorted: &[(&String, &Described<TopLevelTypeSpec>)],
) -> Result<()> {
let mut types = Map::new();
for (name, ty) in types_sorted {
types.insert(name.to_string(), create_top_level_schema(ty));
}
let json = json!({
"$id": "jay_toml_schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$ref": "#/$defs/Config",
"$defs": types,
});
let json = serde_json::to_string_pretty(&json).unwrap();
std::fs::write(
concat!(env!("CARGO_MANIFEST_DIR"), "/spec/spec.generated.json"),
json.as_bytes(),
)?;
Ok(())
}
fn create_top_level_schema(spec: &Described<TopLevelTypeSpec>) -> Value {
match &spec.value {
TopLevelTypeSpec::Variable { variants } => {
let mut cases = vec![];
for variant in variants {
cases.push(create_variant_schema(&variant.description, &variant.value));
}
json!({
"description": spec.description,
"anyOf": cases,
})
}
TopLevelTypeSpec::Single(variant) => create_variant_schema(&spec.description, variant),
}
}
fn create_variant_schema(description: &str, spec: &VariantSpec) -> Value {
macro_rules! spec {
($v:expr) => {
match $v {
RefOrSpec::Ref { name } => return create_ref_spec(description, name),
RefOrSpec::Spec(s) => s,
}
};
}
match spec {
VariantSpec::String(ss) => {
let ss = spec!(ss);
create_string_spec(description, ss)
}
VariantSpec::Number(ns) => {
let ns = spec!(ns);
create_number_spec(description, ns)
}
VariantSpec::Boolean => create_boolean_spec(description),
VariantSpec::Array(s) => {
let s = spec!(s);
create_array_spec(description, s)
}
VariantSpec::Table(ts) => {
let ts = spec!(ts);
match ts {
TableSpec::Tagged { types } => {
let mut variants = vec![];
for (name, ty) in types {
variants.push(create_single_table_spec(
&ty.description,
&ty.value,
Some(name),
));
}
json!({
"description": description,
"anyOf": variants,
})
}
TableSpec::Single(s) => create_single_table_spec(description, s, None),
}
}
}
}
fn create_single_table_spec(
description: &str,
spec: &SingleTableSpec,
type_: Option<&str>,
) -> Value {
let mut properties = Map::new();
let mut required = vec![];
if let Some(type_) = type_ {
properties.insert("type".into(), json!({ "const": type_ }));
required.push("type".into());
}
for (key, val) in &spec.fields {
properties.insert(
key.into(),
create_nestable_type_spec(&val.description, &val.value.kind),
);
if val.value.required {
required.push(key.to_string());
}
}
json!({
"description": description,
"type": "object",
"properties": properties,
"required": required,
})
}
fn create_ref_spec(description: &str, name: &str) -> Value {
let path = format!("#/$defs/{name}");
json!({
"description": description,
"$ref": path,
})
}
fn create_nestable_type_spec(description: &str, spec: &RefOrSpec<NestableTypesSpec>) -> Value {
let spec = match spec {
RefOrSpec::Ref { name } => return create_ref_spec(description, name),
RefOrSpec::Spec(s) => s,
};
match spec {
NestableTypesSpec::String(s) => create_string_spec(description, s),
NestableTypesSpec::Number(s) => create_number_spec(description, s),
NestableTypesSpec::Boolean => create_boolean_spec(description),
NestableTypesSpec::Array(s) => create_array_spec(description, s),
NestableTypesSpec::Map(s) => create_map_spec(description, s),
}
}
fn create_map_spec(description: &str, spec: &MapSpec) -> Value {
json!({
"description": description,
"type": "object",
"additionalProperties": create_nestable_type_spec("", &spec.values),
})
}
fn create_string_spec(description: &str, spec: &StringSpec) -> Value {
let mut res = Map::new();
res.insert("type".into(), json!("string"));
res.insert("description".into(), json!(description));
if let Some(values) = &spec.values {
let strings: Vec<_> = values.iter().map(|v| &v.value.value).collect();
res.insert("enum".into(), json!(strings));
}
res.into()
}
fn create_array_spec(description: &str, spec: &ArraySpec) -> Value {
json!({
"type": "array",
"description": description,
"items": create_nestable_type_spec("", &spec.items),
})
}
fn create_number_spec(description: &str, spec: &NumberSpec) -> Value {
let ty = match spec.integer_only {
true => "integer",
false => "number",
};
let mut res = Map::new();
res.insert("type".into(), json!(ty));
res.insert("description".into(), json!(description));
if let Some(minimum) = spec.minimum {
let key = match spec.exclusive_minimum {
true => "exclusiveMinimum",
false => "minimum",
};
res.insert(key.into(), json!(minimum));
}
res.into()
}
fn create_boolean_spec(description: &str) -> Value {
json!({"type": "boolean", "description": description})
}

27
toml-spec/src/main.rs Normal file
View file

@ -0,0 +1,27 @@
use {
crate::{
json_schema::generate_json_schema,
markdown::generate_markdown,
types::{Described, TopLevelTypeSpec},
},
anyhow::Result,
indexmap::IndexMap,
};
mod json_schema;
mod markdown;
mod types;
fn parse() -> Result<IndexMap<String, Described<TopLevelTypeSpec>>> {
let file = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/spec/spec.yaml"))?;
Ok(serde_yaml::from_str(&file)?)
}
fn main() -> Result<()> {
let types = parse()?;
let mut types_sorted: Vec<_> = types.iter().collect();
types_sorted.sort_by_key(|t| t.0);
generate_markdown(&types_sorted)?;
generate_json_schema(&types_sorted)?;
Ok(())
}

259
toml-spec/src/markdown.rs Normal file
View file

@ -0,0 +1,259 @@
use {
crate::types::{
ArraySpec, Described, NestableTypesSpec, NumberSpec, RefOrSpec, SingleTableSpec,
StringSpec, TableSpec, TopLevelTypeSpec, VariantSpec,
},
anyhow::Result,
std::io::Write,
};
pub fn generate_markdown(types: &[(&String, &Described<TopLevelTypeSpec>)]) -> Result<()> {
const TEMPLATE: &str = include_str!("../spec/template.md");
let mut buf = vec![];
buf.extend_from_slice(TEMPLATE.as_bytes());
for (name, ty) in types {
write_top_level_type_spec(&mut buf, name, ty)?;
}
std::fs::write(
concat!(env!("CARGO_MANIFEST_DIR"), "/spec/spec.generated.md"),
&buf,
)?;
Ok(())
}
fn write_top_level_type_spec(
buf: &mut Vec<u8>,
name: &str,
spec: &Described<TopLevelTypeSpec>,
) -> Result<()> {
writeln!(buf, "<a name=\"types-{name}\"></a>")?;
writeln!(buf, "### `{name}`")?;
writeln!(buf)?;
writeln!(buf, "{}", spec.description.trim())?;
writeln!(buf)?;
match &spec.value {
TopLevelTypeSpec::Variable { variants } => {
writeln!(
buf,
"Values of this type should have one of the following forms:"
)?;
writeln!(buf)?;
for variant in variants {
write!(buf, "#### ")?;
let name = match &variant.value {
VariantSpec::String(_) => "A string",
VariantSpec::Number(_) => "A number",
VariantSpec::Boolean => "A boolean",
VariantSpec::Array(_) => "An array",
VariantSpec::Table(_) => "A table",
};
writeln!(buf, "{name}")?;
writeln!(buf)?;
writeln!(buf, "{}", variant.description.trim())?;
writeln!(buf)?;
write_variant_spec(buf, &variant.value)?;
}
}
TopLevelTypeSpec::Single(variant) => {
let name = match &variant {
VariantSpec::String(_) => "strings",
VariantSpec::Number(_) => "numbers",
VariantSpec::Boolean => "booleans",
VariantSpec::Array(_) => "arrays",
VariantSpec::Table(_) => "tables",
};
writeln!(buf, "Values of this type should be {name}.")?;
writeln!(buf)?;
write_variant_spec(buf, variant)?;
}
}
writeln!(buf)?;
Ok(())
}
fn write_variant_spec(buf: &mut Vec<u8>, spec: &VariantSpec) -> Result<()> {
macro_rules! spec {
($v:expr) => {
match $v {
RefOrSpec::Ref { name } => {
writeln!(buf, "The value should be a [{name}](#types-{name}).")?;
writeln!(buf)?;
return Ok(());
}
RefOrSpec::Spec(s) => s,
}
};
}
match spec {
VariantSpec::String(ss) => {
let ss = spec!(ss);
write_string_spec(buf, ss, "")?;
}
VariantSpec::Number(ns) => {
let ns = spec!(ns);
write_number_spec(buf, ns, "")?;
}
VariantSpec::Boolean => {}
VariantSpec::Array(s) => {
let s = spec!(s);
write_array_spec(buf, s, "")?;
}
VariantSpec::Table(ts) => {
let ts = spec!(ts);
match ts {
TableSpec::Tagged { types } => {
writeln!(buf, "This table is a tagged union. The variant is determined by the `type` field. It takes one of the following values:")?;
writeln!(buf)?;
for (name, spec) in types {
writeln!(buf, "- `{name}`:")?;
writeln!(buf)?;
for line in spec.description.trim().lines() {
writeln!(buf, " {line}")?;
}
writeln!(buf)?;
write_single_table_spec(buf, &spec.value, " ")?;
}
}
TableSpec::Single(s) => {
write_single_table_spec(buf, s, "")?;
}
}
}
}
Ok(())
}
fn write_single_table_spec(buf: &mut Vec<u8>, spec: &SingleTableSpec, pad: &str) -> Result<()> {
writeln!(buf, "{pad}The table has the following fields:")?;
writeln!(buf)?;
for (name, fs) in &spec.fields {
let optional = match fs.value.required {
true => "required",
false => "optional",
};
writeln!(buf, "{pad}- `{name}` ({optional}):")?;
writeln!(buf)?;
for line in fs.description.trim().lines() {
writeln!(buf, "{pad} {line}")?;
}
writeln!(buf)?;
write!(buf, "{pad} The value of this field should be ")?;
let spec = write_nestable_type_spec(buf, &fs.value.kind, false)?;
writeln!(buf, ".")?;
writeln!(buf)?;
if let Some(spec) = spec {
let pad = format!("{pad} ");
write_nestable_type_restrictions(buf, spec, &pad)?;
}
}
Ok(())
}
fn write_nestable_type_spec<'a>(
buf: &mut Vec<u8>,
spec: &'a RefOrSpec<NestableTypesSpec>,
plural: bool,
) -> Result<Option<&'a NestableTypesSpec>> {
let spec = match spec {
RefOrSpec::Ref { name } => {
if plural {
write!(buf, "[{name}s](#types-{name})")?;
} else {
write!(buf, "a [{name}](#types-{name})")?;
}
return Ok(None);
}
RefOrSpec::Spec(s) => s,
};
let name = match (spec, plural) {
(NestableTypesSpec::String(_), false) => "a string",
(NestableTypesSpec::String(_), true) => "strings",
(NestableTypesSpec::Number(_), false) => "a number",
(NestableTypesSpec::Number(_), true) => "numbers",
(NestableTypesSpec::Boolean, false) => "a boolean",
(NestableTypesSpec::Boolean, true) => "booleans",
(NestableTypesSpec::Map(s), _) => {
let name = match plural {
true => "tables",
false => "a table",
};
write!(buf, "{name} whose values are ")?;
return write_nestable_type_spec(buf, &s.values, true);
}
(NestableTypesSpec::Array(s), _) => {
let name = match plural {
true => "arrays",
false => "an array",
};
write!(buf, "{name} of ")?;
return write_nestable_type_spec(buf, &s.items, true);
}
};
write!(buf, "{name}")?;
Ok(Some(spec))
}
fn write_nestable_type_restrictions(
buf: &mut Vec<u8>,
spec: &NestableTypesSpec,
pad: &str,
) -> Result<()> {
match spec {
NestableTypesSpec::String(s) => write_string_spec(buf, s, pad),
NestableTypesSpec::Number(s) => write_number_spec(buf, s, pad),
NestableTypesSpec::Boolean => Ok(()),
NestableTypesSpec::Array(_) => Ok(()),
NestableTypesSpec::Map(_) => Ok(()),
}
}
fn write_string_spec(buf: &mut Vec<u8>, spec: &StringSpec, pad: &str) -> Result<()> {
if let Some(values) = &spec.values {
writeln!(
buf,
"{pad}The string should have one of the following values:"
)?;
writeln!(buf)?;
for value in values {
writeln!(buf, "{pad}- `{}`:", value.value.value)?;
writeln!(buf)?;
for line in value.description.lines() {
writeln!(buf, "{pad} {line}")?;
}
writeln!(buf)?;
}
writeln!(buf)?;
}
Ok(())
}
fn write_array_spec(buf: &mut Vec<u8>, spec: &ArraySpec, pad: &str) -> Result<()> {
write!(buf, "{pad}Each element of this array should be ")?;
let spec = write_nestable_type_spec(buf, &spec.items, false)?;
writeln!(buf, ".")?;
writeln!(buf)?;
if let Some(spec) = spec {
write_nestable_type_restrictions(buf, spec, pad)?;
}
Ok(())
}
fn write_number_spec(buf: &mut Vec<u8>, spec: &NumberSpec, pad: &str) -> Result<()> {
if spec.integer_only {
writeln!(buf, "{pad}The numbers should be integers.")?;
writeln!(buf)?;
}
if let Some(minimum) = spec.minimum {
let greater = match spec.exclusive_minimum {
true => "strictly greater than",
false => "greater than or equal to",
};
writeln!(buf, "{pad}The numbers should be {greater} {minimum}.")?;
writeln!(buf)?;
}
Ok(())
}

184
toml-spec/src/types.rs Normal file
View file

@ -0,0 +1,184 @@
use {
error_reporter::Report,
indexmap::IndexMap,
serde::{
de::{DeserializeOwned, Error},
Deserialize, Deserializer,
},
};
#[derive(Debug, Deserialize)]
pub struct Described<T> {
pub description: String,
#[serde(flatten)]
pub value: T,
}
#[derive(Debug)]
pub enum TopLevelTypeSpec {
Variable {
variants: Vec<Described<VariantSpec>>,
},
Single(VariantSpec),
}
#[derive(Debug)]
pub enum TableSpec {
Tagged {
types: IndexMap<String, Described<SingleTableSpec>>,
},
Single(SingleTableSpec),
}
#[derive(Debug, Deserialize)]
pub struct SingleTableSpec {
pub fields: IndexMap<String, Described<TableFieldSpec>>,
}
#[derive(Debug, Deserialize)]
pub struct TableFieldSpec {
pub required: bool,
#[serde(flatten)]
pub kind: RefOrSpec<NestableTypesSpec>,
}
#[derive(Debug)]
pub enum RefOrSpec<T> {
Ref { name: String },
Spec(T),
}
#[derive(Debug, Deserialize)]
pub struct StringSpec {
pub values: Option<Vec<Described<StringSpecValue>>>,
}
#[derive(Debug, Deserialize)]
pub struct StringSpecValue {
pub value: String,
}
#[derive(Debug, Deserialize)]
pub struct NumberSpec {
#[serde(default)]
pub integer_only: bool,
pub minimum: Option<f64>,
#[serde(default)]
pub exclusive_minimum: bool,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case", tag = "kind")]
pub enum VariantSpec {
String(RefOrSpec<StringSpec>),
Number(RefOrSpec<NumberSpec>),
Boolean,
Array(RefOrSpec<ArraySpec>),
Table(RefOrSpec<TableSpec>),
}
#[derive(Debug, Deserialize)]
pub struct ArraySpec {
pub items: Box<RefOrSpec<NestableTypesSpec>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case", tag = "kind")]
pub enum NestableTypesSpec {
String(StringSpec),
Number(NumberSpec),
Boolean,
Array(ArraySpec),
Map(MapSpec),
}
#[derive(Debug, Deserialize)]
pub struct MapSpec {
pub values: Box<RefOrSpec<NestableTypesSpec>>,
}
impl<'de> Deserialize<'de> for TopLevelTypeSpec {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let v = serde_yaml::Value::deserialize(deserializer)?;
#[derive(Debug, Deserialize)]
struct Variable {
variants: Vec<Described<VariantSpec>>,
}
let variable = Variable::deserialize(&v);
let single = VariantSpec::deserialize(&v);
let res = match (variable, single) {
(Ok(variable), _) => Self::Variable {
variants: variable.variants,
},
(_, Ok(single)) => Self::Single(single),
(Err(e1), Err(e2)) => {
return Err(Error::custom(format!(
"spec must define either variants or a single variant. failures: {} ----- {}",
Report::new(e1),
Report::new(e2)
)))
}
};
Ok(res)
}
}
impl<'de> Deserialize<'de> for TableSpec {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let v = serde_yaml::Value::deserialize(deserializer)?;
#[derive(Debug, Deserialize)]
struct Tagged {
types: IndexMap<String, Described<SingleTableSpec>>,
}
let tagged = Tagged::deserialize(&v);
let single = SingleTableSpec::deserialize(&v);
let res = match (tagged, single) {
(Ok(tagged), _) => Self::Tagged {
types: tagged.types,
},
(_, Ok(single)) => Self::Single(single),
(Err(e1), Err(e2)) => {
return Err(Error::custom(format!(
"spec must define either types or fields. failures: {} ----- {}",
Report::new(e1),
Report::new(e2)
)))
}
};
Ok(res)
}
}
impl<'de, U: DeserializeOwned> Deserialize<'de> for RefOrSpec<U> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let v = serde_yaml::Value::deserialize(deserializer)?;
#[derive(Debug, Deserialize)]
struct Ref {
#[serde(rename = "ref")]
name: String,
}
let name = Ref::deserialize(&v);
let single = U::deserialize(&v);
let res = match (name, single) {
(Ok(name), _) => Self::Ref { name: name.name },
(_, Ok(single)) => Self::Spec(single),
(Err(e1), Err(e2)) => {
return Err(Error::custom(format!(
"spec must define either a ref or a spec. failures: {} ----- {}",
Report::new(e1),
Report::new(e2)
)))
}
};
Ok(res)
}
}