jtd-codegen 0.1.4

Generate code from JSON Typedef schemas
use crate::handlebars_helpers;
use crate::statemgr::StateManager;
use failure::Error;
use handlebars::Handlebars;
use inflector::Inflector;
use jtd::{Form, Schema};
use serde::Serialize;
use std::fs::File;
use std::path::{Path, PathBuf};

#[derive(Debug)]
pub struct Target {
    root_name: String,
    out_dir: PathBuf,
}

#[derive(Debug, Serialize)]
struct TemplateData {
    aliases: Vec<TypeAlias>,
    structs: Vec<Struct>,
}

#[derive(Debug, Serialize)]
struct TypeAlias {
    description: String,
    name: String,
    value: String,
}

#[derive(Debug, Serialize)]
struct Struct {
    description: String,
    name: String,
    members: Vec<Member>,
}

#[derive(Debug, Serialize)]
struct Member {
    description: String,
    name: String,
    required: bool,
    value: String,
}

impl super::Target for Target {
    fn args<'a, 'b>(app: clap::App<'a, 'b>) -> clap::App<'a, 'b> {
        app.arg(
            clap::Arg::with_name("typescript-out")
                .long("typescript-out")
                .help("TypeScript output directory")
                .takes_value(true),
        )
    }

    fn from_args(matches: &clap::ArgMatches) -> Result<Option<Self>, Error> {
        if let Some(out_dir) = matches.value_of("typescript-out") {
            Ok(Some(Self {
                root_name: Path::new(matches.value_of("INPUT").unwrap())
                    .file_stem()
                    .unwrap()
                    .to_str()
                    .unwrap()
                    .trim_end_matches(".jtd")
                    .to_owned(),
                out_dir: PathBuf::from(out_dir),
            }))
        } else {
            Ok(None)
        }
    }

    fn codegen(&self, schema: &Schema) -> Result<(), Error> {
        let mut state = StateManager::new(
            self.root_name.clone(),
            TemplateData {
                aliases: vec![],
                structs: vec![],
            },
        );

        for (name, sub_schema) in &schema.definitions {
            state.with_path_segment(name.clone(), &|state| {
                state.with_must_emit(true, &|state| Self::emit_ast(state, sub_schema))
            });
        }

        state.with_must_emit(true, &|state| Self::emit_ast(state, schema));

        state.data.aliases.sort_by_key(|a| a.name.clone());

        for struct_ in &mut state.data.structs {
            struct_.members.sort_by_key(|v| v.name.clone());
        }

        let mut registry = Handlebars::new();
        registry.register_escape_fn(handlebars::no_escape);
        registry.register_helper("comment", Box::new(handlebars_helpers::comment));

        let mut out = File::create(self.out_dir.join("index.ts"))?;
        registry.render_template_to_write(
            include_str!("template.ts.hbs"),
            &Some(state.data),
            &mut out,
        )?;
        Ok(())
    }
}

impl Target {
    fn emit_ast(state: &mut StateManager<TemplateData>, schema: &Schema) -> String {
        match schema.form {
            Form::Empty => {
                let name = "any".to_owned();

                if state.must_emit() {
                    state.data.aliases.push(TypeAlias {
                        description: description(schema),
                        name: state.name(),
                        value: name.clone(),
                    });
                }

                name
            }
            Form::Ref(jtd::form::Ref {
                ref definition,
                nullable,
            }) => {
                let name = if nullable {
                    format!("({} | undefined)", state.definition_name(&definition))
                } else {
                    state.definition_name(&definition)
                };

                if state.must_emit() {
                    state.data.aliases.push(TypeAlias {
                        description: description(schema),
                        name: state.name(),
                        value: name.clone(),
                    });
                }

                name
            }
            Form::Type(jtd::form::Type {
                ref type_value,
                nullable,
            }) => {
                let name = match type_value {
                    jtd::form::TypeValue::Boolean => "boolean",
                    jtd::form::TypeValue::Float32 => "number",
                    jtd::form::TypeValue::Float64 => "number",
                    jtd::form::TypeValue::Int8 => "number",
                    jtd::form::TypeValue::Uint8 => "number",
                    jtd::form::TypeValue::Int16 => "number",
                    jtd::form::TypeValue::Uint16 => "number",
                    jtd::form::TypeValue::Int32 => "number",
                    jtd::form::TypeValue::Uint32 => "number",
                    jtd::form::TypeValue::String => "string",
                    jtd::form::TypeValue::Timestamp => "string",
                };

                let name = if nullable {
                    format!("({} | undefined)", name)
                } else {
                    name.to_owned()
                };

                if state.must_emit() {
                    state.data.aliases.push(TypeAlias {
                        description: description(schema),
                        name: state.name(),
                        value: name.clone(),
                    })
                }

                name
            }
            Form::Enum(jtd::form::Enum {
                ref values,
                nullable,
            }) => {
                let mut values: Vec<_> = values
                    .into_iter()
                    .map(|value| format!("{:?}", value))
                    .collect();

                values.sort();

                let name = values.join(" | ");
                let name = if nullable {
                    format!("({} | undefined)", name)
                } else {
                    format!("({})", name)
                };

                if state.must_emit() {
                    state.data.aliases.push(TypeAlias {
                        description: description(schema),
                        name: state.name(),
                        value: name.clone(),
                    });
                }

                name
            }
            Form::Elements(jtd::form::Elements {
                ref schema,
                nullable,
            }) => {
                let sub_name = state.with_singularize(true, &|state| {
                    state.with_must_emit(false, &|state| Self::emit_ast(state, schema))
                });

                let name = if nullable {
                    format!("({}[] | undefined)", sub_name)
                } else {
                    format!("{}[]", sub_name)
                };

                if state.must_emit() {
                    state.data.aliases.push(TypeAlias {
                        description: description(schema),
                        name: state.name(),
                        value: name.clone(),
                    });
                }

                name
            }
            Form::Properties(jtd::form::Properties { nullable, .. }) => {
                let strukt = Self::props_to_struct(state, schema);
                state.data.structs.push(strukt);

                if nullable {
                    format!("({} | undefined)", state.name())
                } else {
                    state.name()
                }
            }
            Form::Values(jtd::form::Values {
                ref schema,
                nullable,
            }) => {
                let sub_name = state.with_singularize(true, &|state| {
                    state.with_must_emit(false, &|state| Self::emit_ast(state, schema))
                });

                let name = if nullable {
                    format!("({{[name: string]: {}}} | undefined)", sub_name)
                } else {
                    format!("{{[name: string]: {}}}", sub_name)
                };

                if state.must_emit() {
                    state.data.aliases.push(TypeAlias {
                        description: description(schema),
                        name: state.name(),
                        value: name.clone(),
                    });
                }

                name
            }
            Form::Discriminator(jtd::form::Discriminator {
                ref discriminator,
                ref mapping,
                nullable,
            }) => {
                let mut variants = vec![];
                for (name, schema) in mapping {
                    variants.push(state.with_path_segment(name.clone(), &|state| {
                        let mut strukt = Self::props_to_struct(state, schema);
                        strukt.members.push(Member {
                            description: "".to_owned(),
                            name: discriminator.clone(),
                            required: true,
                            value: format!("{:?}", name),
                        });

                        state.data.structs.push(strukt);
                        state.name()
                    }));
                }

                variants.sort();

                state.data.aliases.push(TypeAlias {
                    description: description(schema),
                    name: state.name(),
                    value: variants.join(" | "),
                });

                if nullable {
                    format!("({} | undefined)", state.name())
                } else {
                    state.name()
                }
            }
        }
    }

    fn props_to_struct(state: &mut StateManager<TemplateData>, schema: &Schema) -> Struct {
        if let Form::Properties(jtd::form::Properties {
            ref required,
            ref optional,
            ..
        }) = schema.form
        {
            let mut members = vec![];
            for (name, schema) in required {
                members.push(Member {
                    description: description(schema),
                    name: name.to_camel_case(),
                    required: true,
                    value: state
                        .with_path_segment(name.clone(), &|state| Self::emit_ast(state, schema)),
                });
            }

            for (name, schema) in optional {
                members.push(Member {
                    description: description(schema),
                    name: name.to_camel_case(),
                    required: false,
                    value: state
                        .with_path_segment(name.clone(), &|state| Self::emit_ast(state, schema)),
                });
            }

            Struct {
                description: description(schema),
                name: state.name(),
                members,
            }
        } else {
            unreachable!("non-properties form schema passed to props_to_struct")
        }
    }
}

fn description(schema: &Schema) -> String {
    schema
        .metadata
        .get("description")
        .and_then(|v| v.as_str())
        .unwrap_or_default()
        .to_owned()
}