use std::hash::RandomState;
use facet::Facet;
use facet_core::Shape;
use facet_error as _;
use heck::ToKebabCase;
use indexmap::IndexMap;
use crate::path::Path;
pub struct Args<'a> {
inner: &'a IndexMap<String, ArgSchema, RandomState>,
}
impl<'a> Args<'a> {
pub fn get(&self, flag_name: &str) -> Option<(&'a String, &'a ArgSchema)> {
self.inner
.iter()
.find(|(key, _)| key.to_kebab_case() == flag_name)
}
pub fn iter(&self) -> impl Iterator<Item = (&'a String, &'a ArgSchema)> {
self.inner.iter()
}
pub fn keys(&self) -> impl Iterator<Item = &'a String> {
self.inner.keys()
}
}
impl<'a> IntoIterator for Args<'a> {
type Item = (&'a String, &'a ArgSchema);
type IntoIter = indexmap::map::Iter<'a, String, ArgSchema>;
fn into_iter(self) -> Self::IntoIter {
self.inner.iter()
}
}
pub(crate) mod error;
pub(crate) mod from_schema;
#[derive(Facet, Debug, Clone)]
#[facet(skip_all_unless_truthy)]
pub struct Schema {
docs: Docs,
args: ArgLevelSchema,
config: Option<ConfigStructSchema>,
special: SpecialFields,
}
#[derive(Facet, Default, Debug, Clone)]
#[facet(skip_all_unless_truthy)]
pub struct SpecialFields {
pub help: Option<Path>,
pub completions: Option<Path>,
pub version: Option<Path>,
}
#[derive(Facet, Debug, Clone)]
#[facet(skip_all_unless_truthy)]
pub struct ArgLevelSchema {
args: IndexMap<String, ArgSchema, RandomState>,
subcommands: IndexMap<String, Subcommand, RandomState>,
subcommand_field_name: Option<String>,
subcommand_optional: bool,
}
#[derive(Facet, Debug, Clone)]
#[facet(skip_all_unless_truthy)]
pub struct ConfigStructSchema {
field_name: Option<String>,
env_prefix: Option<String>,
#[facet(skip)]
shape: &'static Shape,
fields: IndexMap<String, ConfigFieldSchema, RandomState>,
}
#[derive(Facet, Debug, Default, Clone)]
#[facet(skip_all_unless_truthy)]
pub struct Docs {
summary: Option<String>,
details: Option<String>,
}
#[derive(Facet, Debug, Clone)]
#[repr(u8)]
pub enum ScalarType {
Bool,
String,
Integer,
Float,
Other,
}
#[derive(Facet, Debug, Clone)]
#[facet(skip_all_unless_truthy)]
#[repr(u8)]
pub enum LeafKind {
Scalar(ScalarType),
#[allow(dead_code)]
Enum { variants: Vec<String> },
}
#[derive(Facet, Debug, Clone)]
#[facet(skip_all_unless_truthy)]
pub struct LeafSchema {
kind: LeafKind,
#[facet(skip)]
pub(crate) shape: &'static Shape,
}
#[derive(Facet, Debug, Clone)]
#[facet(skip_all_unless_truthy)]
#[repr(u8)]
pub enum ValueSchema {
Leaf(LeafSchema),
Option {
value: Box<ValueSchema>,
#[facet(skip)]
shape: &'static Shape,
},
Vec {
element: Box<ValueSchema>,
#[facet(skip)]
shape: &'static Shape,
},
Struct {
fields: ConfigStructSchema,
#[facet(skip)]
shape: &'static Shape,
},
}
#[derive(Facet, Debug, Clone)]
#[facet(skip_all_unless_truthy)]
pub struct Subcommand {
name: String,
effective_name: String,
docs: Docs,
args: ArgLevelSchema,
is_flattened_tuple: bool,
#[facet(skip)]
shape: &'static Shape,
}
#[derive(Facet, Debug, Clone)]
#[facet(skip_all_unless_truthy)]
pub struct ArgSchema {
name: String,
docs: Docs,
kind: ArgKind,
value: ValueSchema,
label: Option<String>,
required: bool,
multiple: bool,
default: Option<crate::config_value::ConfigValue>,
}
#[derive(Facet, Debug, Clone)]
#[facet(skip_all_unless_truthy)]
#[repr(u8)]
pub enum ArgKind {
Positional,
Named { short: Option<char>, counted: bool },
}
#[derive(Facet, Debug, Clone)]
#[facet(skip_all_unless_truthy)]
pub struct ConfigFieldSchema {
docs: Docs,
sensitive: bool,
env_aliases: Vec<String>,
env_subst: bool,
pub value: ConfigValueSchema,
default: Option<crate::config_value::ConfigValue>,
}
#[derive(Facet, Debug, Clone)]
#[facet(skip_all_unless_truthy)]
pub struct ConfigVecSchema {
shape: &'static Shape,
element: Box<ConfigValueSchema>,
}
#[derive(Facet, Debug, Clone)]
#[facet(skip_all_unless_truthy)]
#[repr(u8)]
pub enum ConfigValueSchema {
Struct(ConfigStructSchema),
Vec(ConfigVecSchema),
Option {
value: Box<ConfigValueSchema>,
shape: &'static Shape,
},
Enum(ConfigEnumSchema),
Leaf(LeafSchema),
}
#[derive(Facet, Debug, Clone)]
#[facet(skip_all_unless_truthy)]
pub struct ConfigEnumSchema {
#[facet(skip)]
shape: &'static Shape,
variants: IndexMap<String, ConfigEnumVariantSchema, RandomState>,
}
#[derive(Facet, Debug, Clone)]
#[facet(skip_all_unless_truthy)]
pub struct ConfigEnumVariantSchema {
docs: Docs,
fields: IndexMap<String, ConfigFieldSchema, RandomState>,
}
pub trait SchemaVisitor {
fn enter_schema(&mut self, _path: &Path, _schema: &Schema) {}
fn enter_arg_level(&mut self, _path: &Path, _args: &ArgLevelSchema) {}
fn enter_arg(&mut self, _path: &Path, _arg: &ArgSchema) {}
fn enter_subcommand(&mut self, _path: &Path, _subcommand: &Subcommand) {}
fn enter_value(&mut self, _path: &Path, _value: &ValueSchema) {}
fn enter_config_struct(&mut self, _path: &Path, _config: &ConfigStructSchema) {}
fn enter_config_value(&mut self, _path: &Path, _value: &ConfigValueSchema) {}
}
impl Schema {
pub fn visit(&self, visitor: &mut impl SchemaVisitor) {
let mut path: Path = Vec::new();
visitor.enter_schema(&path, self);
self.args.visit(visitor, &mut path);
if let Some(config) = &self.config {
path.push("config".to_string());
config.visit(visitor, &mut path);
path.pop();
}
}
}
impl ArgLevelSchema {
fn visit(&self, visitor: &mut impl SchemaVisitor, path: &mut Path) {
visitor.enter_arg_level(path, self);
for (name, arg) in &self.args {
path.push(name.clone());
visitor.enter_arg(path, arg);
arg.value.visit(visitor, path);
path.pop();
}
for (name, sub) in &self.subcommands {
path.push(name.clone());
visitor.enter_subcommand(path, sub);
sub.args.visit(visitor, path);
path.pop();
}
}
}
impl ValueSchema {
fn visit(&self, visitor: &mut impl SchemaVisitor, path: &mut Path) {
visitor.enter_value(path, self);
match self {
ValueSchema::Leaf(_) => {}
ValueSchema::Option { value, .. } => value.visit(visitor, path),
ValueSchema::Vec { element, .. } => element.visit(visitor, path),
ValueSchema::Struct { fields, .. } => fields.visit(visitor, path),
}
}
}
impl ConfigStructSchema {
fn visit(&self, visitor: &mut impl SchemaVisitor, path: &mut Path) {
visitor.enter_config_struct(path, self);
for (name, field) in &self.fields {
path.push(name.clone());
field.value.visit(visitor, path);
path.pop();
}
}
pub fn get_by_path(&self, path: &Path) -> Option<&ConfigValueSchema> {
let mut iter = path.iter();
let first = iter.next()?;
let mut current = &self.fields.get(first)?.value;
for segment in iter {
current = match current {
ConfigValueSchema::Struct(s) => &s.fields.get(segment)?.value,
ConfigValueSchema::Vec(v) => {
segment.parse::<usize>().ok()?;
v.element.as_ref()
}
ConfigValueSchema::Option { value, .. } => value.as_ref(),
ConfigValueSchema::Enum(e) => {
e.variants
.values()
.find_map(|v| v.fields.get(segment))
.map(|f| &f.value)?
}
ConfigValueSchema::Leaf(_) => return None,
};
}
Some(current)
}
}
impl ConfigValueSchema {
fn visit(&self, visitor: &mut impl SchemaVisitor, path: &mut Path) {
visitor.enter_config_value(path, self);
match self {
ConfigValueSchema::Struct(s) => s.visit(visitor, path),
ConfigValueSchema::Vec(v) => v.element.visit(visitor, path),
ConfigValueSchema::Option { value, .. } => value.visit(visitor, path),
ConfigValueSchema::Enum(e) => {
for (name, variant) in &e.variants {
path.push(name.clone());
for (field_name, field) in &variant.fields {
path.push(field_name.clone());
field.value.visit(visitor, path);
path.pop();
}
path.pop();
}
}
ConfigValueSchema::Leaf(_) => {}
}
}
}
impl Schema {
pub fn docs(&self) -> &Docs {
&self.docs
}
pub fn args(&self) -> &ArgLevelSchema {
&self.args
}
pub fn config(&self) -> Option<&ConfigStructSchema> {
self.config.as_ref()
}
pub fn special(&self) -> &SpecialFields {
&self.special
}
}
impl ArgLevelSchema {
pub fn args(&self) -> Args<'_> {
Args { inner: &self.args }
}
pub fn subcommands(&self) -> &IndexMap<String, Subcommand, RandomState> {
&self.subcommands
}
pub fn subcommand_field_name(&self) -> Option<&str> {
self.subcommand_field_name.as_deref()
}
pub fn subcommand_optional(&self) -> bool {
self.subcommand_optional
}
pub fn has_subcommands(&self) -> bool {
!self.subcommands.is_empty()
}
}
impl Docs {
pub fn summary(&self) -> Option<&str> {
self.summary.as_deref()
}
pub fn details(&self) -> Option<&str> {
self.details.as_deref()
}
}
impl ArgKind {
pub fn short(&self) -> Option<char> {
match self {
ArgKind::Named { short, .. } => *short,
ArgKind::Positional => None,
}
}
pub fn is_counted(&self) -> bool {
matches!(self, ArgKind::Named { counted: true, .. })
}
pub fn is_positional(&self) -> bool {
matches!(self, ArgKind::Positional)
}
}
impl ArgSchema {
pub fn name(&self) -> &str {
&self.name
}
pub fn kind(&self) -> &ArgKind {
&self.kind
}
pub fn value(&self) -> &ValueSchema {
&self.value
}
pub fn required(&self) -> bool {
self.required
}
pub fn multiple(&self) -> bool {
self.multiple
}
pub fn docs(&self) -> &Docs {
&self.docs
}
pub fn label(&self) -> Option<&str> {
self.label.as_deref()
}
pub fn default(&self) -> Option<&crate::config_value::ConfigValue> {
self.default.as_ref()
}
}
impl Subcommand {
pub fn cli_name(&self) -> &str {
&self.name
}
pub fn effective_name(&self) -> &str {
&self.effective_name
}
pub fn args(&self) -> &ArgLevelSchema {
&self.args
}
pub fn is_flattened_tuple(&self) -> bool {
self.is_flattened_tuple
}
pub fn docs(&self) -> &Docs {
&self.docs
}
}
impl ConfigStructSchema {
pub fn field_name(&self) -> Option<&str> {
self.field_name.as_deref()
}
pub fn env_prefix(&self) -> Option<&str> {
self.env_prefix.as_deref()
}
pub fn fields(&self) -> &IndexMap<String, ConfigFieldSchema, RandomState> {
&self.fields
}
pub fn shape(&self) -> &'static Shape {
self.shape
}
}
impl ConfigFieldSchema {
pub fn value(&self) -> &ConfigValueSchema {
&self.value
}
pub fn docs(&self) -> &Docs {
&self.docs
}
pub fn is_sensitive(&self) -> bool {
self.sensitive
}
pub fn env_aliases(&self) -> &[String] {
&self.env_aliases
}
pub fn env_subst(&self) -> bool {
self.env_subst
}
pub fn default(&self) -> Option<&crate::config_value::ConfigValue> {
self.default.as_ref()
}
}
impl ConfigVecSchema {
pub fn element(&self) -> &ConfigValueSchema {
&self.element
}
pub fn shape(&self) -> &'static Shape {
self.shape
}
}
impl ConfigEnumSchema {
pub fn variants(&self) -> &IndexMap<String, ConfigEnumVariantSchema, RandomState> {
&self.variants
}
pub fn get_variant(&self, name: &str) -> Option<&ConfigEnumVariantSchema> {
self.variants.get(name)
}
pub fn shape(&self) -> &'static Shape {
self.shape
}
}
impl ConfigEnumVariantSchema {
pub fn fields(&self) -> &IndexMap<String, ConfigFieldSchema, RandomState> {
&self.fields
}
pub fn docs(&self) -> &Docs {
&self.docs
}
}
impl ValueSchema {
pub fn is_bool(&self) -> bool {
matches!(
self,
ValueSchema::Leaf(LeafSchema {
kind: LeafKind::Scalar(ScalarType::Bool),
..
})
)
}
pub fn is_bool_or_vec_of_bool(&self) -> bool {
match self {
ValueSchema::Leaf(LeafSchema {
kind: LeafKind::Scalar(ScalarType::Bool),
..
}) => true,
ValueSchema::Vec { element, .. } => element.is_bool(),
_ => false,
}
}
pub fn inner_if_option(&self) -> &ValueSchema {
match self {
ValueSchema::Option { value, .. } => value.as_ref(),
other => other,
}
}
pub fn type_identifier(&self) -> &'static str {
match self {
ValueSchema::Leaf(leaf) => leaf.shape.type_identifier,
ValueSchema::Option { value, .. } => value.type_identifier(),
ValueSchema::Vec { element, .. } => element.type_identifier(),
ValueSchema::Struct { shape, .. } => shape.type_identifier,
}
}
pub fn is_option(&self) -> bool {
matches!(self, ValueSchema::Option { .. })
}
pub fn enum_variants(&self) -> Option<&[String]> {
match self {
ValueSchema::Leaf(LeafSchema {
kind: LeafKind::Enum { variants },
..
}) => Some(variants.as_slice()),
_ => None,
}
}
}
#[cfg(test)]
#[allow(dead_code)]
mod tests;